# # # 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()