summary refs log tree commit diff stats
path: root/testament/azure.nim
diff options
context:
space:
mode:
Diffstat (limited to 'testament/azure.nim')
-rw-r--r--testament/azure.nim147
1 files changed, 147 insertions, 0 deletions
diff --git a/testament/azure.nim b/testament/azure.nim
new file mode 100644
index 000000000..af65d6a1c
--- /dev/null
+++ b/testament/azure.nim
@@ -0,0 +1,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()