1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
|
#
#
# The Nim Tester
# (c) Copyright 2019 Leorize
#
# Look at license.txt for more info.
# All rights reserved.
import base64, json, httpclient, os, strutils, uri
import specs
const
RunIdEnv = "TESTAMENT_AZURE_RUN_ID"
CacheSize = 8 # How many results should be cached before uploading to
# Azure Pipelines. This prevents throttling that might arise.
proc getAzureEnv(env: string): string =
# Conversion rule at:
# https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables#set-variables-in-pipeline
env.toUpperAscii().replace('.', '_').getEnv
template getRun(): string =
## Get the test run attached to this instance
getEnv(RunIdEnv)
template setRun(id: string) =
## Attach a test run to this instance and its future children
putEnv(RunIdEnv, id)
template delRun() =
## Unattach the test run associtated with this instance and its future children
delEnv(RunIdEnv)
template warning(args: varargs[untyped]) =
## Add a warning to the current task
stderr.writeLine "##vso[task.logissue type=warning;]", args
let
ownRun = not existsEnv RunIdEnv
## Whether the test run is owned by this instance
accessToken = getAzureEnv("System.AccessToken")
## Access token to Azure Pipelines
var
active = false ## Whether the backend should be activated
requestBase: Uri ## Base URI for all API requests
requestHeaders: HttpHeaders ## Headers required for all API requests
results: JsonNode ## A cache for test results before uploading
proc request(api: string, httpMethod: HttpMethod, body = ""): Response {.inline.} =
let client = newHttpClient(timeout = 3000)
defer: close client
result = client.request($(requestBase / api), httpMethod, body, requestHeaders)
if result.code != Http200:
raise newException(CatchableError, "Request failed")
proc init*() =
## Initialize the Azure Pipelines backend.
##
## If an access token is provided and no test run is associated with the
## current instance, this proc will create a test run named after the current
## Azure Pipelines' job name, then associate it to the current testament
## instance and its future children. Should this fail, the backend will be
## disabled.
if isAzure and accessToken.len > 0:
active = true
requestBase = parseUri(getAzureEnv("System.TeamFoundationCollectionUri")) /
getAzureEnv("System.TeamProjectId") / "_apis" ? {"api-version": "5.0"}
requestHeaders = newHttpHeaders {
"Accept": "application/json",
"Authorization": "Basic " & encode(':' & accessToken),
"Content-Type": "application/json"
}
results = newJArray()
if ownRun:
try:
let resp = request(
"test/runs",
HttpPost,
$ %* {
"automated": true,
"build": { "id": getAzureEnv("Build.BuildId") },
"buildPlatform": hostCPU,
"controller": "nim-testament",
"name": getAzureEnv("Agent.JobName")
}
)
setRun $resp.body.parseJson["id"].getInt
except:
warning "Couldn't create test run for Azure Pipelines integration"
# Set run id to empty to prevent child processes from trying to request
# for yet another test run id, which wouldn't be shared with other
# instances.
setRun ""
active = false
elif getRun().len == 0:
# Disable integration if there aren't any valid test run id
active = false
proc uploadAndClear() =
## Upload test results from cache to Azure Pipelines. Then clear the cache
## after.
if results.len > 0:
try:
discard request("test/runs/" & getRun() & "/results", HttpPost, $results)
except:
for i in results:
warning "Couldn't log test result to Azure Pipelines: ",
i["automatedTestName"], ", outcome: ", i["outcome"]
results = newJArray()
proc finalize*() {.noconv.} =
## Finalize the Azure Pipelines backend.
##
## If a test run has been associated and is owned by this instance, it will
## be marked as complete.
if active:
if ownRun:
uploadAndClear()
try:
discard request("test/runs/" & getRun(), HttpPatch,
$ %* {"state": "Completed"})
except:
warning "Couldn't update test run ", getRun(), " on Azure Pipelines"
delRun()
proc addTestResult*(name, category: string; durationInMs: int; errorMsg: string;
outcome: TResultEnum) =
if not active:
return
let outcome = case outcome
of reSuccess: "Passed"
of reDisabled, reJoined: "NotExecuted"
else: "Failed"
results.add(%* {
"automatedTestName": name,
"automatedTestStorage": category,
"durationInMs": durationInMs,
"errorMessage": errorMsg,
"outcome": outcome,
"testCaseTitle": name
})
if results.len > CacheSize:
uploadAndClear()
|