azure.nim 4.8 KB

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