From c21a1768439fa1cba3eaf8a11f6cb161ddde2c32 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos <6349682+vaind@users.noreply.github.com> Date: Tue, 28 Mar 2023 16:38:18 +0200 Subject: [PATCH] feat: sentry-cli integration test scripts (#54) --- ...ter-scripts-tests.yml => script-tests.yml} | 8 +- .github/workflows/updater-workflow-tests.yml | 46 ----- .github/workflows/workflow-tests.yml | 62 ++++++ CHANGELOG.md | 6 + sentry-cli/integration-test/.gitignore | 1 + sentry-cli/integration-test/action.psm1 | 167 ++++++++++++++++ sentry-cli/integration-test/action.yml | 21 +++ .../integration-test/assets/artifact.json | 10 + .../integration-test/assets/artifacts.json | 22 +++ .../assets/associate-dsyms.json | 17 ++ .../assets/debug-info-files.json | 15 ++ .../integration-test/assets/deploy.json | 8 + .../integration-test/assets/release.json | 38 ++++ sentry-cli/integration-test/assets/repos.json | 13 ++ sentry-cli/integration-test/sentry-server.py | 178 ++++++++++++++++++ .../integration-test/tests/action.Tests.ps1 | 45 +++++ updater/scripts/update-dependency.ps1 | 3 + 17 files changed, 610 insertions(+), 50 deletions(-) rename .github/workflows/{updater-scripts-tests.yml => script-tests.yml} (74%) delete mode 100644 .github/workflows/updater-workflow-tests.yml create mode 100644 .github/workflows/workflow-tests.yml create mode 100644 sentry-cli/integration-test/.gitignore create mode 100644 sentry-cli/integration-test/action.psm1 create mode 100644 sentry-cli/integration-test/action.yml create mode 100644 sentry-cli/integration-test/assets/artifact.json create mode 100644 sentry-cli/integration-test/assets/artifacts.json create mode 100644 sentry-cli/integration-test/assets/associate-dsyms.json create mode 100644 sentry-cli/integration-test/assets/debug-info-files.json create mode 100644 sentry-cli/integration-test/assets/deploy.json create mode 100644 sentry-cli/integration-test/assets/release.json create mode 100644 sentry-cli/integration-test/assets/repos.json create mode 100644 sentry-cli/integration-test/sentry-server.py create mode 100644 sentry-cli/integration-test/tests/action.Tests.ps1 diff --git a/.github/workflows/updater-scripts-tests.yml b/.github/workflows/script-tests.yml similarity index 74% rename from .github/workflows/updater-scripts-tests.yml rename to .github/workflows/script-tests.yml index 67aa8a3..c3535ed 100644 --- a/.github/workflows/updater-scripts-tests.yml +++ b/.github/workflows/script-tests.yml @@ -1,12 +1,12 @@ -# This isn't a reusable workflow but an actual CI action for this repo itself - to test scripts. -name: Updater Script Tests +# This isn't a reusable workflow but a CI action for this repo itself - testing the contained workflows & scripts. +name: Script Tests on: push: jobs: - test: - name: ${{ matrix.host }} + updater: + name: Updater @ ${{ matrix.host }} runs-on: ${{ matrix.host }}-latest strategy: fail-fast: false diff --git a/.github/workflows/updater-workflow-tests.yml b/.github/workflows/updater-workflow-tests.yml deleted file mode 100644 index 675a6a1..0000000 --- a/.github/workflows/updater-workflow-tests.yml +++ /dev/null @@ -1,46 +0,0 @@ -# This isn't a reusable workflow but an actual CI action for this repo itself - to test the workflows. -name: Updater Workflow Tests - -on: - push: - -jobs: - create-pr: - uses: ./.github/workflows/updater.yml - with: - path: updater/tests/sentry-cli.properties - name: CLI - pattern: '^2\.0\.' - pr-strategy: update - _workflow_version: ${{ github.sha }} - secrets: - api-token: ${{ github.token }} - - test-args: - uses: ./.github/workflows/updater.yml - with: - path: updater/tests/workflow-args.sh - name: Workflow args test script - runs-on: macos-latest - pattern: '.*' - _workflow_version: ${{ github.sha }} - secrets: - api-token: ${{ github.token }} - - test-outputs: - runs-on: ubuntu-latest - needs: - - create-pr - - test-args - steps: - - run: "[[ '${{ needs.create-pr.outputs.baseBranch }}' == 'main' ]]" - - run: "[[ '${{ needs.create-pr.outputs.originalTag }}' == '2.0.0' ]]" - - run: "[[ '${{ needs.create-pr.outputs.latestTag }}' =~ ^[0-9.]+$ ]]" - - run: "[[ '${{ needs.create-pr.outputs.prUrl }}' =~ ^https://github.com/getsentry/github-workflows/pull/[0-9]+$ ]]" - - run: "[[ '${{ needs.create-pr.outputs.prBranch }}' == 'deps/updater/tests/sentry-cli.properties' ]]" - - - run: "[[ '${{ needs.test-args.outputs.baseBranch }}' == '' ]]" - - run: "[[ '${{ needs.test-args.outputs.originalTag }}' == 'latest' ]]" - - run: "[[ '${{ needs.test-args.outputs.latestTag }}' == 'latest' ]]" - - run: "[[ '${{ needs.test-args.outputs.prUrl }}' == '' ]]" - - run: "[[ '${{ needs.test-args.outputs.prBranch }}' == '' ]]" diff --git a/.github/workflows/workflow-tests.yml b/.github/workflows/workflow-tests.yml new file mode 100644 index 0000000..a94b0f5 --- /dev/null +++ b/.github/workflows/workflow-tests.yml @@ -0,0 +1,62 @@ +# This isn't a reusable workflow but an actual CI action for this repo itself - to test the workflows. +name: Workflow Tests + +on: + push: + +jobs: + updater-create-pr: + uses: ./.github/workflows/updater.yml + with: + path: updater/tests/sentry-cli.properties + name: CLI + pattern: '^2\.0\.' + pr-strategy: update + _workflow_version: ${{ github.sha }} + secrets: + api-token: ${{ github.token }} + + updater-test-args: + uses: ./.github/workflows/updater.yml + with: + path: updater/tests/workflow-args.sh + name: Workflow args test script + runs-on: macos-latest + pattern: '.*' + _workflow_version: ${{ github.sha }} + secrets: + api-token: ${{ github.token }} + + updater-test-outputs: + runs-on: ubuntu-latest + needs: + - updater-create-pr + - updater-test-args + steps: + - run: "[[ '${{ needs.updater-create-pr.outputs.baseBranch }}' == 'main' ]]" + - run: "[[ '${{ needs.updater-create-pr.outputs.originalTag }}' == '2.0.0' ]]" + - run: "[[ '${{ needs.updater-create-pr.outputs.latestTag }}' =~ ^[0-9.]+$ ]]" + - run: "[[ '${{ needs.updater-create-pr.outputs.prUrl }}' =~ ^https://github.com/getsentry/github-workflows/pull/[0-9]+$ ]]" + - run: "[[ '${{ needs.updater-create-pr.outputs.prBranch }}' == 'deps/updater/tests/sentry-cli.properties' ]]" + + - run: "[[ '${{ needs.updater-test-args.outputs.baseBranch }}' == '' ]]" + - run: "[[ '${{ needs.updater-test-args.outputs.originalTag }}' == 'latest' ]]" + - run: "[[ '${{ needs.updater-test-args.outputs.latestTag }}' == 'latest' ]]" + - run: "[[ '${{ needs.updater-test-args.outputs.prUrl }}' == '' ]]" + - run: "[[ '${{ needs.updater-test-args.outputs.prBranch }}' == '' ]]" + + cli-integration: + runs-on: ${{ matrix.host }}-latest + strategy: + fail-fast: false + matrix: + host: + - ubuntu + - macos + - windows + steps: + - uses: actions/checkout@v3 + + - uses: ./sentry-cli/integration-test/ + with: + path: sentry-cli/integration-test/tests/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fb07db..83984b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Sentry-CLI integration test action ([#54](https://github.com/getsentry/github-workflows/pull/54)) + ## 2.6.0 ### Features diff --git a/sentry-cli/integration-test/.gitignore b/sentry-cli/integration-test/.gitignore new file mode 100644 index 0000000..30a8296 --- /dev/null +++ b/sentry-cli/integration-test/.gitignore @@ -0,0 +1 @@ +*-output.txt diff --git a/sentry-cli/integration-test/action.psm1 b/sentry-cli/integration-test/action.psm1 new file mode 100644 index 0000000..5f05ab0 --- /dev/null +++ b/sentry-cli/integration-test/action.psm1 @@ -0,0 +1,167 @@ +# Executes the given block starting a dummy Sentry server that collects and logs requests. +# The block is given the server URL as a first argument. +# Returns the dummy server logs. + +$ServerUri = "http://127.0.0.1:8000" + +class InvokeSentryResult +{ + [string[]]$ServerStdOut + [string[]]$ServerStdErr + [string[]]$ScriptOutput + + # It is common to test debug files uploaded to the server so this function gives you a list. + [string[]]UploadedDebugFiles() + { + $prefix = "upload-dif:" + return @($this.ServerStdOut | Where-Object { $_.StartsWith($prefix) } | ForEach-Object { $_.Substring($prefix.Length).Trim() }) + } + + [bool]HasErrors() + { + return $this.ServerStdErr.Length -gt 0 + } +} + +function IsNullOrEmpty([string] $value) +{ + "$value".Trim().Length -eq 0 +} + +function OutputToArray($output, [string] $uri = $null) +{ + if ($output -isnot [system.array]) + { + $output = ("$output".Trim() -replace "`r`n", "`n") -split "`n" + } + + if (!(IsNullOrEmpty $uri)) + { + $output = $output -replace $uri, "" + } + $output | ForEach-Object { "$_".Trim() } +} + +function RunApiServer([string] $ServerScript, [string] $Uri = $ServerUri) +{ + $result = "" | Select-Object -Property process, outFile, errFile, stop, output, dispose + Write-Host "Starting the $ServerScript on $Uri" -ForegroundColor DarkYellow + $stopwatch = [system.diagnostics.stopwatch]::StartNew() + + $result.outFile = New-TemporaryFile + $result.errFile = New-TemporaryFile + + $result.process = Start-Process "python3" -ArgumentList @("$PSScriptRoot/$ServerScript.py", $Uri) ` + -NoNewWindow -PassThru -RedirectStandardOutput $result.outFile -RedirectStandardError $result.errFile + + $out = New-Object InvokeSentryResult + $out.ServerStdOut = @() + $out.ServerStdErr = @() + + # We must reassign functions as variables to make them available in a block scope together with GetNewClosure(). + $OutputToArray = { OutputToArray $args[0] $args[1] } + $IsNullOrEmpty = { IsNullOrEmpty $args[0] } + + $result.dispose = { + $result.stop.Invoke() + + $stdout = Get-Content $result.outFile -Raw + Write-Host "Server stdout:" -ForegroundColor Yellow + Write-Host $stdout + + $out.ServerStdOut += & $OutputToArray $stdout $Uri + + $stderr = Get-Content $result.errFile -Raw + if (!(& $IsNullOrEmpty $stderr)) + { + Write-Host "Server stderr:" -ForegroundColor Yellow + Write-Host $stderr + $out.ServerStdErr += & $OutputToArray $stderr $Uri + } + + Remove-Item $result.outFile -ErrorAction Continue + Remove-Item $result.errFile -ErrorAction Continue + return $out + }.GetNewClosure() + + $result.stop = { + # Stop the HTTP server + Write-Host "Stopping the $ServerScript ... " -NoNewline + try + { + Write-Host (Invoke-WebRequest -Uri "$Uri/STOP").StatusDescription + } + catch + { + Write-Host "/STOP request failed: $_ - killing the server process instead" + $result.process | Stop-Process -Force -ErrorAction SilentlyContinue + } + $result.process | Wait-Process -Timeout 10 -ErrorAction Continue + $result.stop = {} + }.GetNewClosure() + + $startupFailed = $false + while ($true) + { + Start-Sleep -Milliseconds 100 + try + { + if ((Invoke-WebRequest -Uri "$Uri/_check" -SkipHttpErrorCheck -Method Head).StatusCode -eq 999) + { + $msg = "Server started successfully in $($stopwatch.ElapsedMilliseconds) ms." + Write-Host $additionalOutput -ForegroundColor Green + $out.ServerStdOut += $msg + break; + } + } + catch + {} + if ($stopwatch.ElapsedMilliseconds -gt 60000) + { + $msg = "Server startup timed out." + Write-Warning $msg + $out.ServerStdErr += $msg + $startupFailed = $true; + break; + } + else + { + Write-Host "Waiting for server to become available..." + } + } + + if ($result.process.HasExited -or $startupFailed) + { + $result.stop.Invoke() + $result.dispose.Invoke() + throw Write-Host "Couldn't start the $ServerScript" + } + + return $result +} + +function Invoke-SentryServer([ScriptBlock] $Callback) +{ + # start the server + $httpServer = RunApiServer "sentry-server" + + $result = $null + $output = $null + try + { + # run the test + $output = & $Callback $ServerUri + } + finally + { + $result = $httpServer.dispose.Invoke()[0] + } + + if ($null -ne $result) + { + $result.ScriptOutput = OutputToArray $output + } + return $result +} + +Export-ModuleMember -Function Invoke-SentryServer diff --git a/sentry-cli/integration-test/action.yml b/sentry-cli/integration-test/action.yml new file mode 100644 index 0000000..79d3158 --- /dev/null +++ b/sentry-cli/integration-test/action.yml @@ -0,0 +1,21 @@ +name: Sentry CLI integration test + +description: | + Action to test Sentry CLI integration & symbol upload. This action simply runs all the https://github.com/pester/Pester + tests in the given directory. The tests can make use of a dummy Sentry server that collects uploaded symbols. + This server is made available as a PowerShell module to your tests. + +inputs: + path: + description: The directory containing all the tests. + required: true + +runs: + using: composite + + steps: + - name: Run tests + shell: pwsh + run: | + Import-Module -Name ${{ github.action_path }}/action.psm1 -Force + Invoke-Pester -Output Detailed '${{ inputs.path }}' diff --git a/sentry-cli/integration-test/assets/artifact.json b/sentry-cli/integration-test/assets/artifact.json new file mode 100644 index 0000000..409da18 --- /dev/null +++ b/sentry-cli/integration-test/assets/artifact.json @@ -0,0 +1,10 @@ +{ + "id": "fixture-id", + "sha1": "fixture-sha1", + "name": "fixture-name", + "size": 1, + "dist": null, + "headers": { + "fixture-header-key": "fixture-header-value" + } +} \ No newline at end of file diff --git a/sentry-cli/integration-test/assets/artifacts.json b/sentry-cli/integration-test/assets/artifacts.json new file mode 100644 index 0000000..89391f9 --- /dev/null +++ b/sentry-cli/integration-test/assets/artifacts.json @@ -0,0 +1,22 @@ +[ + { + "id": "6796495645", + "name": "~/dist/bundle.min.js", + "dist": "foo", + "headers": { + "Sourcemap": "dist/bundle.min.js.map" + }, + "size": 497, + "sha1": "2fb719956748ab7ec5ae9bcb47606733f5589b72", + "dateCreated": "2022-05-12T11:08:01.520199Z" + }, + { + "id": "6796495646", + "name": "~/dist/bundle.min.js.map", + "dist": "foo", + "headers": {}, + "size": 1522, + "sha1": "f818059cbf617a8fae9b4e46d08f6c0246bb1624", + "dateCreated": "2022-05-12T11:08:01.496220Z" + } +] \ No newline at end of file diff --git a/sentry-cli/integration-test/assets/associate-dsyms.json b/sentry-cli/integration-test/assets/associate-dsyms.json new file mode 100644 index 0000000..e43dc7f --- /dev/null +++ b/sentry-cli/integration-test/assets/associate-dsyms.json @@ -0,0 +1,17 @@ +{ + "associatedDsymFiles": [ + { + "uuid": null, + "debugId": null, + "objectName": "fixture-objectName", + "cpuName": "fixture-cpuName", + "sha1": "fixture-sha1", + "data": { + "type": null, + "features": [ + "fixture-feature" + ] + } + } + ] +} \ No newline at end of file diff --git a/sentry-cli/integration-test/assets/debug-info-files.json b/sentry-cli/integration-test/assets/debug-info-files.json new file mode 100644 index 0000000..cdb3d25 --- /dev/null +++ b/sentry-cli/integration-test/assets/debug-info-files.json @@ -0,0 +1,15 @@ +[ + { + "uuid": null, + "debugId": null, + "objectName": "fixture-objectName", + "cpuName": "fixture-cpuName", + "sha1": "fixture-sha1", + "data": { + "type": null, + "features": [ + "fixture-feature" + ] + } + } +] \ No newline at end of file diff --git a/sentry-cli/integration-test/assets/deploy.json b/sentry-cli/integration-test/assets/deploy.json new file mode 100644 index 0000000..e79d3e4 --- /dev/null +++ b/sentry-cli/integration-test/assets/deploy.json @@ -0,0 +1,8 @@ +{ + "id": "1", + "environment": "production", + "dateStarted": null, + "dateFinished": "2022-01-01T12:00:00.000000Z", + "name": "fixture-deploy", + "url": null +} \ No newline at end of file diff --git a/sentry-cli/integration-test/assets/release.json b/sentry-cli/integration-test/assets/release.json new file mode 100644 index 0000000..aaea2f8 --- /dev/null +++ b/sentry-cli/integration-test/assets/release.json @@ -0,0 +1,38 @@ +{ + "dateReleased": "2022-01-01T12:00:00.000000Z", + "newGroups": 0, + "commitCount": 0, + "url": null, + "data": {}, + "lastDeploy": null, + "deployCount": 0, + "dateCreated": "2022-01-01T12:00:00.000000Z", + "lastEvent": null, + "version": "1.1.0", + "firstEvent": null, + "lastCommit": null, + "shortVersion": "1.1.0", + "authors": [], + "owner": null, + "versionInfo": { + "buildHash": null, + "version": { + "raw": "1.1.0" + }, + "description": "1.1.0", + "package": null + }, + "ref": null, + "projects": [ + { + "name": "Sentry Test App", + "platform": "ruby", + "slug": "sentry-test-app", + "platforms": [ + "ruby" + ], + "newGroups": 0, + "id": 1234567 + } + ] +} \ No newline at end of file diff --git a/sentry-cli/integration-test/assets/repos.json b/sentry-cli/integration-test/assets/repos.json new file mode 100644 index 0000000..239c74c --- /dev/null +++ b/sentry-cli/integration-test/assets/repos.json @@ -0,0 +1,13 @@ +[ + { + "id": "1", + "name": "sentry/repo", + "url": null, + "provider": { + "id": "1", + "name": "fixture-name" + }, + "status": "fixture-status", + "dateCreated": "2022-01-01T12:00:00.000000Z" + } +] \ No newline at end of file diff --git a/sentry-cli/integration-test/sentry-server.py b/sentry-cli/integration-test/sentry-server.py new file mode 100644 index 0000000..c80ac89 --- /dev/null +++ b/sentry-cli/integration-test/sentry-server.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 + +from http import HTTPStatus +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +import time +from urllib.parse import urlparse +import sys +import threading +import binascii +import json + +uri = urlparse(sys.argv[1] if len(sys.argv) > 1 else 'http://127.0.0.1:8000') +apiOrg = 'org' +apiProject = 'project' +version = '1.1.0' +appIdentifier = 'app' + + +class Handler(BaseHTTPRequestHandler): + body = None + + def do_HEAD(self): + if self.path == "/_check": + self.writeResponse(999, "text/plain", "") + else: + self.writeNoApiMatchesError() + + self.flushLogs() + + def do_GET(self): + self.start_response() + + if self.path == "/STOP": + print("HTTP server stopping!") + self.writeResponse(HTTPStatus.OK, "text/plain", "") + self.flushLogs() + threading.Thread(target=self.server.shutdown).start() + + elif self.isApi('api/0'): + self.writeJSON('{"version":"0","auth":null,"user":null}') + elif self.isApi('api/0/organizations/{}/chunk-upload/'.format(apiOrg)): + self.writeJSON('{"url":"' + uri.geturl() + self.path + '",' + '"chunkSize":8388608,"chunksPerRequest":64,"maxFileSize":2147483648,' + '"maxRequestSize":33554432,"concurrency":1,"hashAlgorithm":"sha1","compression":["gzip"],' + '"accept":["debug_files","release_files","pdbs","sources","bcsymbolmaps","il2cpp","portablepdbs"]}') + elif self.isApi('/api/0/organizations/{}/repos/?cursor='.format(apiOrg)): + self.writeJSONFile("assets/repos.json") + elif self.isApi('/api/0/organizations/{}/releases/{}@{}/previous-with-commits/'.format(apiOrg, appIdentifier, version)): + self.writeJSON('{ }') + elif self.isApi('/api/0/projects/{}/{}/releases/{}/files/?cursor='.format(apiOrg, apiProject, version)): + self.writeJSONFile("assets/artifacts.json") + else: + self.writeNoApiMatchesError() + + self.flushLogs() + + def do_POST(self): + self.start_response() + + if self.isApi('api/0/projects/{}/{}/files/difs/assemble/'.format(apiOrg, apiProject)): + # Request body example: + # { + # "9a01653a...":{"name":"UnityPlayer.dylib","debug_id":"eb4a7644-...","chunks":["f84d3907945cdf41b33da8245747f4d05e6ffcb4", ...]}, + # "4185e454...":{"name":"UnityPlayer.dylib","debug_id":"86d95b40-...","chunks":[...]} + # } + # Response body to let the CLI know we have the symbols already (we don't need to test the actual upload): + # { + # "9a01653a...":{"state":"ok","missingChunks":[]}, + # "4185e454...":{"state":"ok","missingChunks":[]} + # } + jsonRequest = json.loads(self.body) + jsonResponse = '{' + for key, value in jsonRequest.items(): + jsonResponse += '"{}"'.format(key) + jsonResponse += ':{"state":"ok","missingChunks":[]},' + sys.stdout.write(" upload-dif: {}\n".format(value['name'])) + jsonResponse = jsonResponse.rstrip(',') + '}' + self.writeJSON(jsonResponse) + elif self.isApi('api/0/projects/{}/{}/releases/'.format(apiOrg, apiProject)): + self.writeJSONFile("assets/release.json") + elif self.isApi('/api/0/organizations/{}/releases/{}@{}/deploys/'.format(apiOrg, appIdentifier, version)): + self.writeJSONFile("assets/deploy.json") + elif self.isApi('/api/0/projects/{}/{}/releases/{}@{}/files/'.format(apiOrg, apiProject, appIdentifier, version)): + self.writeJSONFile("assets/artifact.json") + elif self.isApi('/api/0/organizations/{}/releases/{}/assemble/'.format(apiOrg, version)): + self.writeJSON('{"state":"ok","missingChunks":[],"detail":null}') + elif self.isApi('/api/0/projects/{}/{}/files/dsyms/'.format(apiOrg, apiProject)): + self.writeJSONFile("assets/debug-info-files.json") + elif self.isApi('/api/0/projects/{}/{}/files/dsyms/associate/'.format(apiOrg, apiProject)): + self.writeJSONFile("assets/associate-dsyms.json") + elif self.isApi('/api/0/projects/{}/{}/reprocessing/'.format(apiOrg, apiProject)): + self.writeJSON('{ }') + elif self.isApi('api/0/organizations/{}/chunk-upload/'.format(apiOrg)): + self.writeJSON('{ }') + else: + self.writeNoApiMatchesError() + + self.flushLogs() + + def do_PUT(self): + self.start_response() + + if self.isApi('/api/0/organizations/{}/releases/{}@{}/'.format(apiOrg, appIdentifier, version)): + self.writeJSONFile("assets/release.json") + elif self.isApi('/api/0/projects/{}/{}/releases/{}@{}/'.format(apiOrg, apiProject, appIdentifier, version)): + self.writeJSONFile("assets/release.json") + else: + self.writeNoApiMatchesError() + + self.flushLogs() + + def start_response(self): + self.body = None + self.log_request() + + def log_request(self, size=None): + body = self.body = self.requestBody() + + log_line = self.requestline + if size: + log_line += " ({} bytes)".format(size) + # if body: + # log_line += "\n " + self.body[0:min(1000, len(body))] + + log_line += '\n' + sys.stdout.write(log_line) + + # Note: this may only be called once during a single request - can't `.read()` the same stream again. + def requestBody(self): + if self.command == "POST" and 'Content-Length' in self.headers: + length = int(self.headers['Content-Length']) + content = self.rfile.read(length) + try: + return content.decode("utf-8") + except: + return binascii.hexlify(bytearray(content)) + return None + + def isApi(self, api: str): + if self.path.strip('/') == api.strip('/'): + # sys.stdout.write("Matched API endpoint {}\n".format(api)) + return True + return False + + def writeNoApiMatchesError(self): + err = "Error: no API matched {} '{}'".format(self.command, self.path) + self.log_error(err) + self.writeResponse(HTTPStatus.NOT_IMPLEMENTED, + "text/plain", err) + + def writeJSONFile(self, file_name: str): + json_file = open(file_name, "r") + self.writeJSON(json_file.read()) + json_file.close() + + def writeJSON(self, string: str): + self.writeResponse(HTTPStatus.OK, "application/json", string) + + def writeResponse(self, code: HTTPStatus, type: str, body: str): + self.send_response_only(code) + self.send_header("Content-type", type) + self.send_header("Content-Length", len(body)) + self.end_headers() + self.wfile.write(str.encode(body)) + + def flushLogs(self): + sys.stdout.flush() + sys.stderr.flush() + + +print("HTTP server listening on {}".format(uri.geturl())) +print("To stop the server, execute a GET request to {}/STOP".format(uri.geturl())) + +try: + httpd = ThreadingHTTPServer((uri.hostname, uri.port), Handler) + target = httpd.serve_forever() +except KeyboardInterrupt: + pass diff --git a/sentry-cli/integration-test/tests/action.Tests.ps1 b/sentry-cli/integration-test/tests/action.Tests.ps1 new file mode 100644 index 0000000..b7bd015 --- /dev/null +++ b/sentry-cli/integration-test/tests/action.Tests.ps1 @@ -0,0 +1,45 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# In CI, the module is expected to be loaded +if (!(Test-Path env:CI )) +{ + Import-Module $PSScriptRoot/../action.psm1 -Force +} + +Describe 'Invoke-SentryServer' { + It "works fine with a simple callback" { + $result = Invoke-SentryServer { + Param([string]$url) + $url | Should -Be "http://127.0.0.1:8000" + "custom script output" + } + Should -ActualValue $result.ServerStdOut -HaveType [string[]] + Should -ActualValue $result.ServerStdErr -HaveType [string[]] + Should -ActualValue $result.ScriptOutput -HaveType [string[]] + $result.ServerStdErr.Length | Should -Be 0 + Should -ActualValue $result.HasErrors() -BeFalse + $result.ServerStdOut.Length | Should -BeGreaterThan 1 + $result.ServerStdOut[0] | Should -Match "Server started successfully in [0-9]+ ms." + $result.ServerStdOut | Should -Contain 'HTTP server listening on ' + $result.ScriptOutput | Should -Be "custom script output" + } + + It "rethrows an exception and recovers" { + { Invoke-SentryServer { throw "hello there" } } | Should -Throw "hello there" + $result = Invoke-SentryServer {} + $result.ServerStdOut | Should -Contain 'HTTP server listening on ' + } + + It "collects debug-files uploads" { + $result = Invoke-SentryServer { + Param([string]$url) + Invoke-WebRequest -Uri "$url/api/0/projects/org/project/files/difs/assemble/" -Method Post ` + -Body '{"9a01653a":{"name":"file3.dylib","debug_id":"eb4a7644","chunks":["f84d"]},"abcd":{"name":"file2.so","debug_id":"foo","chunks":["ab"]}}' + Invoke-WebRequest -Uri "$url/api/0/projects/org/project/files/difs/assemble/" -Method Post ` + -Body '{"9a01653a":{"name":"file1.dll","debug_id":"aa","chunks":["def"]}}' + } + Should -ActualValue $result.HasErrors() -BeFalse + $result.UploadedDebugFiles() | Should -Be @('file3.dylib', 'file2.so', 'file1.dll') + } +} diff --git a/updater/scripts/update-dependency.ps1 b/updater/scripts/update-dependency.ps1 index 6691f83..bfa0ba4 100644 --- a/updater/scripts/update-dependency.ps1 +++ b/updater/scripts/update-dependency.ps1 @@ -45,6 +45,9 @@ if (-not $isSubmodule) if (Get-Command 'chmod' -ErrorAction SilentlyContinue) { chmod +x $Path + if ($LastExitCode -ne 0) { + throw "chmod failed"; + } } try {