diff --git a/utils/flakiness-dashboard/.gitignore b/utils/flakiness-dashboard/.gitignore new file mode 100644 index 0000000000..f6b6a51cd4 --- /dev/null +++ b/utils/flakiness-dashboard/.gitignore @@ -0,0 +1,44 @@ +bin +obj +csx +.vs +edge +Publish + +*.swp +*.user +*.suo +*.cscfg +*.Cache +project.lock.json + +/packages +/TestResults + +/tools/NuGet.exe +/App_Data +/secrets +/data +.secrets +appsettings.json +local.settings.json + +node_modules +dist + +# Local python packages +.python_packages/ + +# Python Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class diff --git a/utils/flakiness-dashboard/README.md b/utils/flakiness-dashboard/README.md new file mode 100644 index 0000000000..57e323d8b6 --- /dev/null +++ b/utils/flakiness-dashboard/README.md @@ -0,0 +1,4 @@ +# Flakiness Dashboard Backend + +This directory contains source code for the Azure function that we use to aggregate test reports. +The data is consumed by https://devops.aslushnikov.com/flakiness.html diff --git a/utils/flakiness-dashboard/host.json b/utils/flakiness-dashboard/host.json new file mode 100644 index 0000000000..b7a3a7c11d --- /dev/null +++ b/utils/flakiness-dashboard/host.json @@ -0,0 +1,21 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensions": { + "queues": { + "batchSize": 1, + "newBatchThreshold": 0 + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[1.*, 2.0.0)" + } +} diff --git a/utils/flakiness-dashboard/package-lock.json b/utils/flakiness-dashboard/package-lock.json new file mode 100644 index 0000000000..00f7ccf7e0 --- /dev/null +++ b/utils/flakiness-dashboard/package-lock.json @@ -0,0 +1,315 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@azure/abort-controller": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.0.1.tgz", + "integrity": "sha512-wP2Jw6uPp8DEDy0n4KNidvwzDjyVV2xnycEIq7nPzj1rHyb/r+t3OPeNT1INZePP2wy5ZqlwyuyOMTi0ePyY1A==", + "requires": { + "tslib": "^1.9.3" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@azure/core-asynciterator-polyfill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.0.tgz", + "integrity": "sha512-kmv8CGrPfN9SwMwrkiBK9VTQYxdFQEGe0BmQk+M8io56P9KNzpAxcWE/1fxJj7uouwN4kXF0BHW8DNlgx+wtCg==" + }, + "@azure/core-auth": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.1.3.tgz", + "integrity": "sha512-A4xigW0YZZpkj1zK7dKuzbBpGwnhEcRk6WWuIshdHC32raR3EQ1j6VA9XZqE+RFsUgH6OAmIK5BWIz+mZjnd6Q==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-tracing": "1.0.0-preview.8", + "@opentelemetry/api": "^0.6.1", + "tslib": "^2.0.0" + }, + "dependencies": { + "@azure/core-tracing": { + "version": "1.0.0-preview.8", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.8.tgz", + "integrity": "sha512-ZKUpCd7Dlyfn7bdc+/zC/sf0aRIaNQMDuSj2RhYRFe3p70hVAnYGp3TX4cnG2yoEALp/LTj/XnZGQ8Xzf6Ja/Q==", + "requires": { + "@opencensus/web-types": "0.0.7", + "@opentelemetry/api": "^0.6.1", + "tslib": "^1.10.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@opentelemetry/api": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-0.6.1.tgz", + "integrity": "sha512-wpufGZa7tTxw7eAsjXJtiyIQ42IWQdX9iUQp7ACJcKo1hCtuhLU+K2Nv1U6oRwT1oAlZTE6m4CgWKZBhOiau3Q==", + "requires": { + "@opentelemetry/context-base": "^0.6.1" + } + } + } + }, + "@azure/core-http": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-1.1.9.tgz", + "integrity": "sha512-wM0HMRNQaE2NtTHb+9FXF7uxUqaAHFTMVu6OzlEll6gUGybcDqM7+9Oklp33BhEfq+ZumpCoqxq3njNbMHuf/w==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.1.3", + "@azure/core-tracing": "1.0.0-preview.9", + "@azure/logger": "^1.0.0", + "@opentelemetry/api": "^0.10.2", + "@types/node-fetch": "^2.5.0", + "@types/tunnel": "^0.0.1", + "form-data": "^3.0.0", + "node-fetch": "^2.6.0", + "process": "^0.11.10", + "tough-cookie": "^4.0.0", + "tslib": "^2.0.0", + "tunnel": "^0.0.6", + "uuid": "^8.1.0", + "xml2js": "^0.4.19" + } + }, + "@azure/core-lro": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-1.0.2.tgz", + "integrity": "sha512-Yr0JD7GKryOmbcb5wHCQoQ4KCcH5QJWRNorofid+UvudLaxnbCfvKh/cUfQsGUqRjO9L/Bw4X7FP824DcHdMxw==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-http": "^1.1.1", + "events": "^3.0.0", + "tslib": "^1.10.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@azure/core-paging": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.1.3.tgz", + "integrity": "sha512-his7Ah40ThEYORSpIAwuh6B8wkGwO/zG7gqVtmSE4WAJ46e36zUDXTKReUCLBDc6HmjjApQQxxcRFy5FruG79A==", + "requires": { + "@azure/core-asynciterator-polyfill": "^1.0.0" + } + }, + "@azure/core-tracing": { + "version": "1.0.0-preview.9", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.9.tgz", + "integrity": "sha512-zczolCLJ5QG42AEPQ+Qg9SRYNUyB+yZ5dzof4YEc+dyWczO9G2sBqbAjLB7IqrsdHN2apkiB2oXeDKCsq48jug==", + "requires": { + "@opencensus/web-types": "0.0.7", + "@opentelemetry/api": "^0.10.2", + "tslib": "^2.0.0" + } + }, + "@azure/logger": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.0.tgz", + "integrity": "sha512-g2qLDgvmhyIxR3JVS8N67CyIOeFRKQlX/llxYJQr1OSGQqM3HTpVP8MjmjcEKbL/OIt2N9C9UFaNQuKOw1laOA==", + "requires": { + "tslib": "^1.9.3" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@azure/storage-blob": { + "version": "12.2.1", + "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.2.1.tgz", + "integrity": "sha512-erqCSmDL8b/AHZi94nq+nCE+2whQmvBDkAv4N9uic0MRac/gRyZqnsqkfrun/gr2rZo+qVtnMenwkkE3roXn8Q==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-http": "^1.1.6", + "@azure/core-lro": "^1.0.2", + "@azure/core-paging": "^1.1.1", + "@azure/core-tracing": "1.0.0-preview.9", + "@azure/logger": "^1.0.0", + "@opentelemetry/api": "^0.10.2", + "events": "^3.0.0", + "tslib": "^2.0.0" + } + }, + "@opencensus/web-types": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@opencensus/web-types/-/web-types-0.0.7.tgz", + "integrity": "sha512-xB+w7ZDAu3YBzqH44rCmG9/RlrOmFuDPt/bpf17eJr8eZSrLt7nc7LnWdxM9Mmoj/YKMHpxRg28txu3TcpiL+g==" + }, + "@opentelemetry/api": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-0.10.2.tgz", + "integrity": "sha512-GtpMGd6vkzDMYcpu2t9LlhEgMy/SzBwRnz48EejlRArYqZzqSzAsKmegUK7zHgl+EOIaK9mKHhnRaQu3qw20cA==", + "requires": { + "@opentelemetry/context-base": "^0.10.2" + }, + "dependencies": { + "@opentelemetry/context-base": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-base/-/context-base-0.10.2.tgz", + "integrity": "sha512-hZNKjKOYsckoOEgBziGMnBcX0M7EtstnCmwz5jZUOUYwlZ+/xxX6z3jPu1XVO2Jivk0eLfuP9GP+vFD49CMetw==" + } + } + }, + "@opentelemetry/context-base": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-base/-/context-base-0.6.1.tgz", + "integrity": "sha512-5bHhlTBBq82ti3qPT15TRxkYTFPPQWbnkkQkmHPtqiS1XcTB69cEKd3Jm7Cfi/vkPoyxapmePE9tyA7EzLt8SQ==" + }, + "@types/node": { + "version": "14.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.1.tgz", + "integrity": "sha512-D2/Xwp9c49JhIWq7SIrdvoYyGdq6yXkr5FTldN4rus9XljYFBul6P2epAID8xepOpL4ffcx09C05FZGK/1AIkw==" + }, + "@types/node-fetch": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.7.tgz", + "integrity": "sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==", + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "@types/tunnel": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.1.tgz", + "integrity": "sha512-AOqu6bQu5MSWwYvehMXLukFHnupHrpZ8nvgae5Ggie9UwzDR1CCwoXgSSWNZJuyOlCdfdsWMA5F2LlmvyoTv8A==", + "requires": { + "@types/node": "*" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "events": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", + "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==" + }, + "form-data": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "tough-cookie": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.1.2" + } + }, + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + }, + "tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "uuid": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", + "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==" + }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + } + } +} diff --git a/utils/flakiness-dashboard/package.json b/utils/flakiness-dashboard/package.json new file mode 100644 index 0000000000..8404c7347b --- /dev/null +++ b/utils/flakiness-dashboard/package.json @@ -0,0 +1,12 @@ +{ + "name": "", + "version": "", + "description": "", + "scripts": { + "test": "echo \"No tests yet...\"" + }, + "author": "", + "dependencies": { + "@azure/storage-blob": "^12.2.1" + } +} diff --git a/utils/flakiness-dashboard/processing/function.json b/utils/flakiness-dashboard/processing/function.json new file mode 100644 index 0000000000..e756a94cc1 --- /dev/null +++ b/utils/flakiness-dashboard/processing/function.json @@ -0,0 +1,11 @@ +{ + "bindings": [ + { + "type": "blobTrigger", + "direction": "in", + "name": "newBlob", + "path": "uploads/{name}", + "connection": "" + } + ] +} diff --git a/utils/flakiness-dashboard/processing/host.json b/utils/flakiness-dashboard/processing/host.json new file mode 100644 index 0000000000..7a24f01b78 --- /dev/null +++ b/utils/flakiness-dashboard/processing/host.json @@ -0,0 +1,13 @@ +{ + "version": "2.0", + "extensions": { + "queues": { + "batchSize": 1, + "newBatchThreshold": 0 + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[1.*, 2.0.0)" + } +} diff --git a/utils/flakiness-dashboard/processing/index.js b/utils/flakiness-dashboard/processing/index.js new file mode 100644 index 0000000000..ed6d7bfd05 --- /dev/null +++ b/utils/flakiness-dashboard/processing/index.js @@ -0,0 +1,173 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the 'License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const { BlobServiceClient } = require("@azure/storage-blob"); +const zlib = require('zlib'); +const path = require('path'); +const util = require('util'); + +const gzipAsync = util.promisify(zlib.gzip); +const gunzipAsync = util.promisify(zlib.gunzip); + +const blobServiceClient = BlobServiceClient.fromConnectionString(process.env.AzureWebJobsStorage); + +const DASHBOARD_CONTAINER = 'dashboards'; +const DASHBOARD_NAME = 'main.json'; +const DASHBOARD_VERSION = 1; + +class SimpleBlob { + static async create(container, blobName) { + const dashboardContainerClient = await blobServiceClient.getContainerClient(DASHBOARD_CONTAINER); + return new SimpleBlob(dashboardContainerClient); + } + + constructor(containerClient) { + this._blobClient = containerClient.getBlobClient(DASHBOARD_NAME); + this._blockBlobClient = this._blobClient.getBlockBlobClient(); + } + + async download() { + if (!await this._blobClient.exists()) + return undefined; + const response = await this._blobClient.download(); + const responseStream = response.readableStreamBody; + const buffer = await new Promise((resolve, reject) => { + const chunks = []; + responseStream.on('data', data => chunks.push(data instanceof Buffer ? data : Buffer.from(data))); + responseStream.on('end', () => resolve(Buffer.concat(chunks))); + responseStream.on('error', reject); + }); + const properties = await this._blobClient.getProperties(); + const content = properties.contentEncoding.toLowerCase().trim() === 'gzip' ? await gunzipAsync(buffer) : buffer.toString('utf8'); + return JSON.parse(content); + } + + async uploadGzipped(data) { + const content = JSON.stringify(data); + const zipped = await gzipAsync(content); + await this._blockBlobClient.upload(zipped, Buffer.byteLength(zipped), { + blobHTTPHeaders: { + blobContentEncoding: 'gzip', + blobContentType: 'application/json; charset=UTF-8', + } + }); + } +} + +async function deleteUploadBlob(blobName) { + // First we do - delete the blob. + const containerClient = await blobServiceClient.getContainerClient('uploads'); + await containerClient.deleteBlob(blobName, {}); +} + +function flattenSpecs(suite, result = []) { + if (suite.suites) { + for (const child of suite.suites) + flattenSpecs(child, result); + } + for (const spec of suite.specs || []) + result.push(spec); + return result; +} + +class Dashboard { + constructor() { + this._runs = []; + } + + initialize(jsonData) { + if (jsonData.version !== DASHBOARD_VERSION) { + // Run migrations here! + } + this._runs = jsonData.buildbotRuns; + } + + addReport(report) { + // We cannot use linenumber to identify specs since line numbers + // might be different across commits. + const getSpecId = spec => spec.file + ' @@@ ' + spec.title; + + const faultySpecIds = new Set(); + for (const run of this._runs) { + for (const spec of run.specs) + faultySpecIds.add(getSpecId(spec)); + } + const specs = []; + for (const spec of flattenSpecs(report)) { + // Filter out specs that didn't have a single test that was run in the + // given shard. + if (spec.tests.every(test => test.runs.length === 1 && !test.runs[0].status)) + continue; + const hasFlakyAnnotation = spec.tests.some(test => test.annotations.some(a => a.type === 'flaky')); + + if (!spec.ok || hasFlakyAnnotation || faultySpecIds.has(getSpecId(spec))) + specs.push(spec); + } + if (specs.length) { + this._runs.push({ + metadata: report.metadata, + specs, + }); + } + return specs.length; + } + + serialize(maxCommits = 100) { + const shaToTimestamp = new Map(); + for (const run of this._runs) + shaToTimestamp.set(run.metadata.commitSHA, run.metadata.commitTimestamp); + const commits = [...shaToTimestamp].sort(([sha1, ts1], [sha2, ts2]) => ts2 - ts1).slice(0, maxCommits); + const commitsSet = new Set(commits.map(([sha, ts]) => sha)); + return { + version: DASHBOARD_VERSION, + timestamp: Date.now(), + buildbotRuns: this._runs.filter(run => commitsSet.has(run.metadata.commitSHA)), + }; + } +} + + +module.exports = async function(context) { + const timestamp = Date.now(); + const blobName = context.bindingData.name; + // First thing we do - delete the blob. + await deleteUploadBlob(blobName); + + const dashboardBlob = await SimpleBlob.create(); + const dashboardData = await dashboardBlob.download(); + const dashboard = new Dashboard(); + if (dashboardData) + dashboard.initialize(dashboardData); + + try { + const data = await gunzipAsync(context.bindings.newBlob); + const report = JSON.parse(data.toString('utf8')); + const addedSpecs = dashboard.addReport(report); + await dashboardBlob.uploadGzipped(dashboard.serialize()); + context.log(` + ===== started ===== + SHA: ${report.metadata.commitSHA} + URL: ${report.metadata.runURL} + timestamp: ${report.metadata.commitTimestamp} + added specs: ${addedSpecs} + ===== complete in ${Date.now() - timestamp}ms ===== + `); + } catch (e) { + context.log(e); + return; + } +} +