diff --git a/docs/devguide/docs/swagger-docs.yaml b/docs/devguide/docs/swagger-docs.yaml index 82d09ba7e..31e95d54d 100644 --- a/docs/devguide/docs/swagger-docs.yaml +++ b/docs/devguide/docs/swagger-docs.yaml @@ -1124,6 +1124,32 @@ paths: application/json: schema: $ref: '#/components/schemas/error_response' + # Report Export + '/v1/{test_id}/reports/{report_id}/export/{file_format}': + get: + operationId: export-report + tags: + - Reports + summary: Exports a report to a file in the required format + description: Exports a report to a file in the required format. + responses: + "200": + description: Success + "400": + description: Unsupported File Format + # Export Comparison Report + '/v1/tests/reports/compare/export/{file_format}': + get: + operationId: export-comparison-report + tags: + - Reports + summary: Exports a comparison report. + description: Exports a comparison report from multiple tests. + responses: + "200": + description: Success + "400": + description: Unsupported File Format # Config /v1/config: get: diff --git a/docs/openapi3.yaml b/docs/openapi3.yaml index b064cc3fc..102467853 100644 --- a/docs/openapi3.yaml +++ b/docs/openapi3.yaml @@ -1124,6 +1124,76 @@ paths: application/json: schema: $ref: '#/components/schemas/error_response' + # Report Export + '/v1/{test_id}/reports/{report_id}/export/{file_format}': + get: + operationId: export-report + tags: + - Reports + parameters: + - in: path + name: test_id + description: The test id. + required: true + schema: + type: string + format: uuid + example: 4bf5d7ab-f310-4a64-8ec2-d65c06188ec1 + - in: path + name: report_id + description: The id of the report to retrieve. + required: true + schema: + type: string + - in: path + name: file_format + description: The file format for export. + required: true + schema: + type: string + enum: [csv] + summary: Exports a report to a file in the required format + description: Exports a report to a file in the required format. + responses: + "200": + description: Success + "400": + description: Unsupported File Format + # Export Comparison Report + '/v1/tests/reports/compare/export/{file_format}': + get: + operationId: export-comparison-report + tags: + - Reports + parameters: + - in: path + name: file_format + description: The file format for export. + required: true + schema: + type: string + enum: [csv] + - in: query + name: report_ids + description: List of report IDs that are part of comparison + schema: + type: array + style: form + explode: false + - in: query + name: test_ids + description: List of test IDs that are part of comparison + schema: + type: array + style: form + explode: false + summary: Exports a comparison report. + description: Exports a comparison report from multiple tests. + responses: + "200": + description: Success + "400": + description: Unsupported File Format # Config /v1/config: get: diff --git a/package-lock.json b/package-lock.json index 45ae4168a..c4abf5617 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1021,6 +1021,11 @@ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", "dev": true }, + "@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" + }, "@sinonjs/commons": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.7.1.tgz", @@ -1056,6 +1061,14 @@ "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "dev": true }, + "@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "requires": { + "defer-to-connect": "^1.0.1" + } + }, "@types/color-name": { "version": "1.1.1", "resolved": "http://npm.zooz.co:8083/@types%2fcolor-name/-/color-name-1.1.1.tgz", @@ -1238,7 +1251,6 @@ "version": "3.1.1", "resolved": "http://npm.zooz.co:8083/anymatch/-/anymatch-3.1.1.tgz", "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "dev": true, "requires": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -1247,8 +1259,7 @@ "normalize-path": { "version": "3.0.0", "resolved": "http://npm.zooz.co:8083/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" } } }, @@ -1741,8 +1752,7 @@ "binary-extensions": { "version": "2.0.0", "resolved": "http://npm.zooz.co:8083/binary-extensions/-/binary-extensions-2.0.0.tgz", - "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", - "dev": true + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==" }, "bl": { "version": "1.2.3", @@ -1890,7 +1900,6 @@ "version": "3.0.2", "resolved": "http://npm.zooz.co:8083/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "requires": { "fill-range": "^7.0.1" } @@ -1990,6 +1999,44 @@ "unset-value": "^1.0.0" } }, + "cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, "cachedir": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.2.0.tgz", @@ -2171,7 +2218,6 @@ "version": "3.3.0", "resolved": "http://npm.zooz.co:8083/chokidar/-/chokidar-3.3.0.tgz", "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", - "dev": true, "requires": { "anymatch": "~3.1.1", "braces": "~3.0.2", @@ -2186,14 +2232,12 @@ "is-extglob": { "version": "2.1.1", "resolved": "http://npm.zooz.co:8083/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, "is-glob": { "version": "4.0.1", "resolved": "http://npm.zooz.co:8083/is-glob/-/is-glob-4.0.1.tgz", "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, "requires": { "is-extglob": "^2.1.1" } @@ -2201,8 +2245,7 @@ "normalize-path": { "version": "3.0.0", "resolved": "http://npm.zooz.co:8083/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" } } }, @@ -2317,6 +2360,14 @@ "shallow-clone": "^3.0.0" } }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "requires": { + "mimic-response": "^1.0.0" + } + }, "cls-bluebird": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cls-bluebird/-/cls-bluebird-2.1.0.tgz", @@ -3908,6 +3959,14 @@ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "requires": { + "mimic-response": "^1.0.0" + } + }, "dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -3963,6 +4022,11 @@ "strip-bom": "^3.0.0" } }, + "defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -4177,8 +4241,7 @@ "dotenv": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", - "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", - "dev": true + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" }, "dotgitignore": { "version": "2.1.0", @@ -4195,6 +4258,11 @@ "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.2.tgz", "integrity": "sha512-fmrwR04lsniq/uSr8yikThDTrM7epXHBAAjH9TbeH3rEA8tdCO7mRzB9hdmdGyJCxF8KERo9CITcm3kGuoyMhg==" }, + "downloadjs": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/downloadjs/-/downloadjs-1.4.7.tgz", + "integrity": "sha1-9p+W+UDg0FU9rCkROYZaPNAQHjw=" + }, "driftless": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/driftless/-/driftless-2.0.3.tgz", @@ -4242,8 +4310,7 @@ "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" }, "encodeurl": { "version": "1.0.2", @@ -4423,6 +4490,11 @@ "es6-symbol": "^3.1.1" } }, + "escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==" + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -5395,7 +5467,6 @@ "version": "7.0.1", "resolved": "http://npm.zooz.co:8083/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "requires": { "to-regex-range": "^5.0.1" } @@ -5722,7 +5793,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", - "dev": true, "optional": true }, "fsu": { @@ -6133,7 +6203,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "dev": true, "requires": { "is-glob": "^4.0.1" }, @@ -6141,14 +6210,12 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, "is-glob": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, "requires": { "is-extglob": "^2.1.1" } @@ -6376,6 +6443,11 @@ } } }, + "has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==" + }, "hasha": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hasha/-/hasha-3.0.0.tgz", @@ -6425,6 +6497,11 @@ "readable-stream": "^3.1.1" } }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, "http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", @@ -6586,6 +6663,11 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=" + }, "ignore-walk": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", @@ -6848,7 +6930,6 @@ "version": "2.1.0", "resolved": "http://npm.zooz.co:8083/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "requires": { "binary-extensions": "^2.0.0" } @@ -6981,8 +7062,7 @@ "is-number": { "version": "7.0.0", "resolved": "http://npm.zooz.co:8083/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, "is-obj": { "version": "1.0.1", @@ -7097,6 +7177,11 @@ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" }, + "is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==" + }, "isarray": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", @@ -7311,6 +7396,11 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" + }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -7448,6 +7538,14 @@ "safe-buffer": "^5.0.1" } }, + "keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "requires": { + "json-buffer": "3.0.0" + } + }, "kind-of": { "version": "6.0.3", "resolved": "http://npm.zooz.co:8083/kind-of/-/kind-of-6.0.3.tgz", @@ -8006,6 +8104,11 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, "min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -8686,6 +8789,433 @@ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.10.tgz", "integrity": "sha512-j+pS9CURhPgk6r0ENr7dji+As2xZiHSvZeVnzKniLOw1eRAyM/7flP0u65tCnsapV8JFu+t0l/5VeHsCZEeh9g==" }, + "nodemon": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.4.tgz", + "integrity": "sha512-Ltced+hIfTmaS28Zjv1BM552oQ3dbwPqI4+zI0SLgq+wpJhSyqgYude/aZa/3i31VCQWMfXJVxvu86abcam3uQ==", + "requires": { + "chokidar": "^3.2.2", + "debug": "^3.2.6", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.0.4", + "pstree.remy": "^1.1.7", + "semver": "^5.7.1", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.2", + "update-notifier": "^4.0.0" + }, + "dependencies": { + "ansi-align": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", + "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", + "requires": { + "string-width": "^3.0.0" + }, + "dependencies": { + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "boxen": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", + "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", + "requires": { + "ansi-align": "^3.0.0", + "camelcase": "^5.3.1", + "chalk": "^3.0.0", + "cli-boxes": "^2.2.0", + "string-width": "^4.1.0", + "term-size": "^2.1.0", + "type-fest": "^0.8.1", + "widest-line": "^3.1.0" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" + }, + "cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==" + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "requires": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + } + }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "requires": { + "is-obj": "^2.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "global-dirs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz", + "integrity": "sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A==", + "requires": { + "ini": "^1.3.5" + } + }, + "got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "requires": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + } + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-installed-globally": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", + "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", + "requires": { + "global-dirs": "^2.0.1", + "is-path-inside": "^3.0.1" + } + }, + "is-npm": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", + "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==" + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" + }, + "is-path-inside": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", + "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==" + }, + "latest-version": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "requires": { + "package-json": "^6.3.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "package-json": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", + "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "requires": { + "got": "^9.6.0", + "registry-auth-token": "^4.0.0", + "registry-url": "^5.0.0", + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "registry-auth-token": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.0.tgz", + "integrity": "sha512-P+lWzPrsgfN+UEpDS3U8AQKg/UjZX6mQSJueZj3EK+vNESoqBSpBUD3gmu4sF9lOsjXWjF11dQKUqemf3veq1w==", + "requires": { + "rc": "^1.2.8" + } + }, + "registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "requires": { + "rc": "^1.2.8" + } + }, + "semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "requires": { + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "term-size": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz", + "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==" + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" + }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "requires": { + "crypto-random-string": "^2.0.0" + } + }, + "update-notifier": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", + "integrity": "sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==", + "requires": { + "boxen": "^4.2.0", + "chalk": "^3.0.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.3.1", + "is-npm": "^4.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.0.0", + "pupa": "^2.0.1", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + } + }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "requires": { + "prepend-http": "^2.0.0" + } + }, + "widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "requires": { + "string-width": "^4.0.0" + } + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" + } + } + }, "nopt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", @@ -8715,6 +9245,11 @@ "remove-trailing-separator": "^1.0.1" } }, + "normalize-url": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" + }, "npm-bundled": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", @@ -9100,6 +9635,11 @@ "os-tmpdir": "^1.0.0" } }, + "p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" + }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -9346,8 +9886,7 @@ "picomatch": { "version": "2.2.2", "resolved": "http://npm.zooz.co:8083/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" }, "pidusage": { "version": "1.2.0", @@ -9546,6 +10085,11 @@ "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" }, + "pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" + }, "pump": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", @@ -9560,6 +10104,14 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, + "pupa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.0.1.tgz", + "integrity": "sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA==", + "requires": { + "escape-goat": "^2.0.0" + } + }, "q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -9748,7 +10300,6 @@ "version": "3.2.0", "resolved": "http://npm.zooz.co:8083/readdirp/-/readdirp-3.2.0.tgz", "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", - "dev": true, "requires": { "picomatch": "^2.0.4" } @@ -10149,6 +10700,14 @@ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", "dev": true }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "requires": { + "lowercase-keys": "^1.0.0" + } + }, "restore-cursor": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", @@ -11959,6 +12518,11 @@ } } }, + "to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" + }, "to-regex": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", @@ -11975,7 +12539,6 @@ "version": "5.0.1", "resolved": "http://npm.zooz.co:8083/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "requires": { "is-number": "^7.0.0" } @@ -11990,6 +12553,24 @@ "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", "integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=" }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "requires": { + "nopt": "~1.0.10" + }, + "dependencies": { + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "requires": { + "abbrev": "1" + } + } + } + }, "tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -12100,6 +12681,14 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "requires": { + "is-typedarray": "^1.0.0" + } + }, "uglify-js": { "version": "3.10.1", "resolved": "http://npm.zooz.co:8083/uglify-js/-/uglify-js-3.10.1.tgz", @@ -12115,6 +12704,14 @@ "bluebird": "^3.7.2" } }, + "undefsafe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", + "integrity": "sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==", + "requires": { + "debug": "^2.2.0" + } + }, "underscore": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", diff --git a/src/reports/controllers/reportsController.js b/src/reports/controllers/reportsController.js index 67227478f..a0563ed55 100644 --- a/src/reports/controllers/reportsController.js +++ b/src/reports/controllers/reportsController.js @@ -2,7 +2,9 @@ 'use strict'; const aggregateReportGenerator = require('../models/aggregateReportGenerator'); const reports = require('../models/reportsManager'); +const reportExporter = require('../models/reportExporter'); const stats = require('../models/statsManager'); +const exportHelper = require('../helpers/exportReportHelper'); module.exports.getAggregateReport = async function (req, res, next) { let reportInput; @@ -87,3 +89,39 @@ module.exports.postStats = async (req, res, next) => { } return res.status(204).json(); }; + +module.exports.getExportedReport = async(req, res, next) => { + let exportedReport; + let reportInput; + try { + reportInput = await aggregateReportGenerator.createAggregateReport(req.params.test_id, req.params.report_id); + exportedReport = await reportExporter.exportReport(reportInput, req.params.file_format); + } catch (err){ + return next(err); + } + const fileName = exportHelper.getExportedReportName(reportInput, req.params.file_format); + res.setHeader('Content-disposition', 'attachment; filename=' + fileName); + res.set('Content-Type', exportHelper.getContentType(req.params.file_format)); + return res.send(exportedReport); +}; + +module.exports.getExportedCompareReport = async(req, res, next) => { + let exportedCompareReport; + const aggregateReportArray = []; + try { + const { reportIds, testIds } = exportHelper.processCompareReportsInput(req.query); + + for (const index in reportIds){ + const result = await aggregateReportGenerator.createAggregateReport(testIds[index], reportIds[index]); + aggregateReportArray.push(result); + } + exportedCompareReport = await reportExporter.exportCompareReport(aggregateReportArray, req.params.file_format); + } catch (err) { + return next(err); + } + + const fileName = exportHelper.getCompareReportName(aggregateReportArray, req.params.file_format); + res.setHeader('Content-disposition', 'attachment; filename=' + fileName); + res.set('Content-Type', exportHelper.getContentType(req.params.file_format)); + res.send(exportedCompareReport); +}; diff --git a/src/reports/helpers/exportReportHelper.js b/src/reports/helpers/exportReportHelper.js new file mode 100644 index 000000000..15f801c44 --- /dev/null +++ b/src/reports/helpers/exportReportHelper.js @@ -0,0 +1,48 @@ +'use strict'; + +module.exports.getExportedReportName = (reportData, fileFormat) => { + const testName = reportData.test_name; + const reportId = reportData.report_id; + const startTime = reportData.start_time.toISOString(); + return testName + '_' + reportId + '_' + startTime + '.' + fileFormat; +}; + +module.exports.getContentType = (fileFormat) => { + const mapping = { + csv: 'text/csv' + }; + return mapping[fileFormat]; +}; + +module.exports.processCompareReportsInput = (query) => { + const reportIds = query.report_ids[0].split(','); + const testIds = query.test_ids[0].split(','); + // Validate length of arrays + if (reportIds.length != testIds.length){ + const error = new Error('Test and Report IDs length mismatch'); + error.statusCode = 400; + throw error; + } + return { reportIds: reportIds, testIds: testIds }; +}; + +module.exports.getCompareReportName = (aggregateReportArray, fileFormat) => { + let fileName = ''; + for (const index in aggregateReportArray){ + if (index == aggregateReportArray.length - 1){ + fileName += aggregateReportArray[index].test_name; + } else { + fileName += aggregateReportArray[index].test_name + '_'; + } + } + fileName += '_comparison_'; + for (const index in aggregateReportArray){ + if (index == aggregateReportArray.length - 1){ + fileName += aggregateReportArray[index].report_id; + } else { + fileName += aggregateReportArray[index].report_id + '_'; + } + } + fileName += '.' + fileFormat; + return fileName; +}; diff --git a/src/reports/models/reportExporter.js b/src/reports/models/reportExporter.js new file mode 100644 index 000000000..d6c5257e1 --- /dev/null +++ b/src/reports/models/reportExporter.js @@ -0,0 +1,137 @@ +function generateCSVReport(jsonReport){ + let outputCSV = ''; + // Header Row + let first_entry = true; + for (const index in jsonReport.headers){ + if (first_entry){ + outputCSV += jsonReport.headers[index]; + first_entry = false; + } else { + outputCSV += ',' + jsonReport.headers[index]; + } + } + // Data Rows + for (const index in jsonReport.data){ + first_entry = true; + outputCSV += '\n'; + for (const key in jsonReport.data[index]){ + if (first_entry){ + if (jsonReport.data[index][key] === undefined){ + jsonReport.data[index][key] = 0; + } + outputCSV += jsonReport.data[index][key]; + first_entry = false; + } else { + if (jsonReport.data[index][key] === undefined){ + jsonReport.data[index][key] = 0; + } + outputCSV += ',' + jsonReport.data[index][key]; + } + } + } + return outputCSV; +} + +function generateJSONReport(report){ + const dataExtracted = { + headers: [ + 'timestamp', + 'timemillis', + 'median', + 'p95', + 'p99', + 'rps' + ], + data: {} + }; // Fixed headers + + const startTime = new Date(report.start_time); + const data = report.intermediates; + + // Dynamic status code headers + const statusHeaders = new Set(); + + for (const index in data){ + const time_diff = new Date(data[index].timestamp) - startTime; + const newEntry = { + timestamp: data[index].timestamp, + timemillis: time_diff - (time_diff % 30000), + median: data[index].latency.median, + p95: data[index].latency.p95, + p99: data[index].latency.p99, + rps: data[index].rps.mean + }; + for (const code in data[index].codes){ + const header = 'status_' + code.toString(); + if (statusHeaders.has(header) === false){ + statusHeaders.add(header); + } + newEntry[header] = data[index].codes[code]; + } + dataExtracted.data[newEntry.timemillis] = newEntry; + } + statusHeaders.forEach((entry) => { + dataExtracted.headers.push(entry); + }); + return dataExtracted; +} + +module.exports.exportReport = async(aggregateReport, fileFormat) => { + switch (fileFormat){ + case 'csv':{ + const jsonReport = generateJSONReport(aggregateReport); + return generateCSVReport(jsonReport); + } + default:{ + const error = new Error('Unsupported file format'); + error.statusCode = 400; + throw error; + } + } +}; + +function nextChar(c) { + return String.fromCharCode(((c.charCodeAt(0) + 1 - 65) % 25) + 65); +} + +function generateJSONCompareReport(aggregateReports){ + const data = { headers: ['timestamp', 'timemillis'], data: {} }; + let character = 'A'; + for (const index in aggregateReports){ + const result = generateJSONReport(aggregateReports[index]); + // Update headers set + for (const headerIndex in result.headers){ + if (result.headers[headerIndex] != 'timestamp' && result.headers[headerIndex] != 'timemillis'){ + data.headers.push(character + '_' + result.headers[headerIndex]); + } + } + for (const entry in result.data){ + if (data.data[entry] === undefined){ + // new timestamp + data.data[entry] = { timestamp: result.data[entry].timestamp, timemillis: entry }; + } + delete result.data[entry].timestamp; + delete result.data[entry].timemillis; + + for (const key in result.data[entry]){ + data.data[entry][character + '_' + key] = result.data[entry][key]; + } + } + character = nextChar(character); + } + return data; +} + +module.exports.exportCompareReport = async(aggregateReports, fileFormat) => { + switch (fileFormat){ + case 'csv':{ + const jsonCompareReport = generateJSONCompareReport(aggregateReports); + return generateCSVReport(jsonCompareReport); + } + default:{ + const error = new Error('Unsupported file format'); + error.statusCode = 400; + throw error; + } + } +}; diff --git a/src/reports/routes/reportsRoute.js b/src/reports/routes/reportsRoute.js index 134620c5f..1387454a2 100644 --- a/src/reports/routes/reportsRoute.js +++ b/src/reports/routes/reportsRoute.js @@ -13,5 +13,6 @@ router.get('/:test_id/reports/', swaggerValidator.validate, reports.getReports); router.get('/last_reports/', swaggerValidator.validate, reports.getLastReports); router.post('/:test_id/reports/', swaggerValidator.validate, reports.postReport); router.post('/:test_id/reports/:report_id/stats', swaggerValidator.validate, reports.postStats); - +router.get('/:test_id/reports/:report_id/export/:file_format', swaggerValidator.validate, reports.getExportedReport); +router.get('/reports/compare/export/:file_format', swaggerValidator.validate, reports.getExportedCompareReport); module.exports = router; diff --git a/tests/integration-tests/reports/helpers/requestCreator.js b/tests/integration-tests/reports/helpers/requestCreator.js index f14947176..bdee845ba 100644 --- a/tests/integration-tests/reports/helpers/requestCreator.js +++ b/tests/integration-tests/reports/helpers/requestCreator.js @@ -16,7 +16,9 @@ module.exports = { getReports, editReport, getLastReports, - getAggregatedReport + getAggregatedReport, + getExportedReport, + getExportedCompareReport, }; async function init() { @@ -66,6 +68,42 @@ function getReport(testId, reportId) { }); } +function getExportedReport(testId, reportId, fileFormat) { + return request(testApp).get(`/v1/tests/${testId}/reports/${reportId}/export/${fileFormat}`) + .set(HEADERS) + .expect(function (res) { + return res; + }); +} + +/* + reportMetaData: Data related to a report: + Structure: + { + report_ids:[], + test_ids:[], + } +*/ +function getExportedCompareReport(fileFormat, reportMetaData) { + let url = `/v1/tests/reports/compare/export/${fileFormat}`; + let reportIdsAsCSV = ""; + let testIdsAsCSV = ""; + for (let index = 0; index < reportMetaData.report_ids.length; index++){ + reportIdsAsCSV+=reportMetaData["report_ids"][index]; + testIdsAsCSV+=reportMetaData["test_ids"][index]; + if (index < reportMetaData.report_ids.length -1){ + reportIdsAsCSV+=","; + testIdsAsCSV+=","; + } + } + let request_string = "report_ids="+reportIdsAsCSV+"&test_ids="+testIdsAsCSV; + return request(testApp).get(url+"?"+request_string) + .set(HEADERS) + .expect(function (res) { + return res; + }); +} + function deleteReport(testId, reportId) { return request(testApp).delete(`/v1/tests/${testId}/reports/${reportId}`) .set(HEADERS) diff --git a/tests/integration-tests/reports/reportsApi-test.js b/tests/integration-tests/reports/reportsApi-test.js index 0bf3cd5e5..f1c5c2217 100644 --- a/tests/integration-tests/reports/reportsApi-test.js +++ b/tests/integration-tests/reports/reportsApi-test.js @@ -14,7 +14,7 @@ const constants = require('../../../src/reports/utils/constants'); const mailhogHelper = require('./mailhog/mailhogHelper'); -let testId, reportId, jobId, runnerId, firstRunner, secondRunner, jobBody, minimalReportBody; +let testId, testIdA, testIdB, reportId, reportIdA, reportIdB, jobId, jobIdA, jobIdB, runnerId, runnerIdA, runnerIdB, firstRunner, secondRunner, jobBody, minimalReportBody, minimalReportBodyA, minimalReportBodyB; describe('Integration tests for the reports api', function () { this.timeout(10000); @@ -29,6 +29,16 @@ describe('Integration tests for the reports api', function () { should(response.statusCode).eql(201); should(response.body).have.key('id'); testId = response.body.id; + + const responseA = await testsRequestCreator.createTest(requestBody, {}); + should(responseA.statusCode).eql(201); + should(responseA.body).have.key('id'); + testIdA = responseA.body.id; + + const responseB = await testsRequestCreator.createTest(requestBody, {}); + should(responseB.statusCode).eql(201); + should(responseB.body).have.key('id'); + testIdB = responseB.body.id; }); beforeEach(async function () { @@ -383,6 +393,7 @@ describe('Integration tests for the reports api', function () { }); }); }); + describe('Get report', function () { const getReportTestId = uuid(); let reportId; @@ -532,6 +543,272 @@ describe('Integration tests for the reports api', function () { }); }); }); + + describe('Get exported reports', function () { + before(async function () { + const jobResponse = await createJob(testId); + jobId = jobResponse.body.id; + }); + beforeEach(async function () { + reportId = uuid(); + runnerId = uuid(); + const requestBody = require('../../testExamples/Basic_test'); + const response = await testsRequestCreator.createTest(requestBody, {}); + should(response.statusCode).be.eql(201); + should(response.body).have.key('id'); + testId = response.body.id; + + minimalReportBody = { + runner_id: runnerId, + test_type: 'basic', + report_id: reportId, + revision_id: uuid(), + job_id: jobId, + test_name: 'integration-test', + test_description: 'doing some integration testing', + start_time: Date.now().toString(), + last_updated_at: Date.now().toString(), + test_configuration: { + enviornment: 'test', + duration: 10, + arrival_rate: 20 + } + }; + + const reportResponse = await reportsRequestCreator.createReport(testId, minimalReportBody); + should(reportResponse.statusCode).be.eql(201); + }); + + it('Post full cycle stats and export report', async function () { + const phaseStartedStatsResponse = await reportsRequestCreator.postStats(testId, reportId, statsGenerator.generateStats('started_phase', runnerId)); + should(phaseStartedStatsResponse.statusCode).be.eql(204); + + const getReport = await reportsRequestCreator.getReport(testId, reportId); + should(getReport.statusCode).be.eql(200); + const testStartTime = new Date(getReport.body.start_time); + const statDateFirst = new Date(testStartTime).setSeconds(testStartTime.getSeconds() + 20); + let intermediateStatsResponse = await reportsRequestCreator.postStats(testId, reportId, statsGenerator.generateStats('intermediate', runnerId, statDateFirst, 600)); + should(intermediateStatsResponse.statusCode).be.eql(204); + let getReportResponse = await reportsRequestCreator.getReport(testId, reportId); + let report = getReportResponse.body; + should(report.avg_rps).eql(30); + + const statDateSecond = new Date(testStartTime).setSeconds(testStartTime.getSeconds() + 40); + intermediateStatsResponse = await reportsRequestCreator.postStats(testId, reportId, statsGenerator.generateStats('intermediate', runnerId, statDateSecond, 200)); + should(intermediateStatsResponse.statusCode).be.eql(204); + getReportResponse = await reportsRequestCreator.getReport(testId, reportId); + report = getReportResponse.body; + should(report.avg_rps).eql(20); + + const statDateThird = new Date(testStartTime).setSeconds(testStartTime.getSeconds() + 60); + const doneStatsResponse = await reportsRequestCreator.postStats(testId, reportId, statsGenerator.generateStats('done', runnerId, statDateThird)); + should(doneStatsResponse.statusCode).be.eql(204); + getReportResponse = await reportsRequestCreator.getReport(testId, reportId); + should(getReportResponse.statusCode).be.eql(200); + report = getReportResponse.body; + should(report.avg_rps).eql(13.33); + + const getExportedReportResponse = await reportsRequestCreator.getExportedReport(testId, reportId, 'csv'); + should(getExportedReportResponse.statusCode).be.eql(200); + + const contentDisposition = new RegExp('attachment; filename=integration-test_[a-f0-9\-]+_.*\.csv'); + const contentType = new RegExp('text/csv; charset=utf-8'); + const receivedDisposition = getExportedReportResponse.header['content-disposition']; + const receivedType = getExportedReportResponse.header['content-type']; + + should(contentDisposition.test(receivedDisposition)).be.eql(true); + should(contentType.test(receivedType)).be.eql(true); + const EXPORTED_REPORT_HEADER = 'timestamp,timemillis,median,p95,p99,rps,status_200'; + const reportLines = getExportedReportResponse.text.split('\n'); + const headers = reportLines[0]; + should(headers).eql(EXPORTED_REPORT_HEADER); + }); + + describe('export report with no stats', function () { + it('should return error 404', async function () { + const getReportResponse = await reportsRequestCreator.getExportedReport(testId, reportId, 'csv'); + should(getReportResponse.statusCode).be.eql(404); + }); + }); + + describe('export report of non existent report', function () { + it('should return error 404', async function () { + const getReportResponse = await reportsRequestCreator.getExportedReport(testId, 'null', 'csv'); + should(getReportResponse.statusCode).be.eql(404); + }); + }); + }); + + describe('Get exported comparison report', function () { + before(async function () { + const jobResponseA = await createJob(testIdA); + jobIdA = jobResponseA.body.id; + + const jobResponseB = await createJob(testIdB); + jobIdB = jobResponseB.body.id; + }); + beforeEach(async function () { + reportIdA = uuid(); + runnerIdA = uuid(); + const requestBodyA = require('../../testExamples/Basic_test'); + const responseA = await testsRequestCreator.createTest(requestBodyA, {}); + should(responseA.statusCode).be.eql(201); + should(responseA.body).have.key('id'); + testIdA = responseA.body.id; + + minimalReportBodyA = { + runner_id: runnerIdA, + test_type: 'basic', + report_id: reportIdA, + revision_id: uuid(), + job_id: jobIdA, + test_name: 'integration-test', + test_description: 'doing some integration testing', + start_time: Date.now().toString(), + last_updated_at: Date.now().toString(), + test_configuration: { + enviornment: 'test', + duration: 10, + arrival_rate: 200 + } + }; + + const reportResponseA = await reportsRequestCreator.createReport(testIdA, minimalReportBodyA); + should(reportResponseA.statusCode).be.eql(201); + + reportIdB = uuid(); + runnerIdB = uuid(); + const requestBodyB = require('../../testExamples/Basic_test'); + const responseB = await testsRequestCreator.createTest(requestBodyB, {}); + should(responseB.statusCode).be.eql(201); + should(responseB.body).have.key('id'); + testIdB = responseB.body.id; + + minimalReportBodyB = { + runner_id: runnerIdB, + test_type: 'basic', + report_id: reportIdB, + revision_id: uuid(), + job_id: jobIdB, + test_name: 'integration-test', + test_description: 'doing some integration testing', + start_time: Date.now().toString(), + last_updated_at: Date.now().toString(), + test_configuration: { + enviornment: 'test', + duration: 10, + arrival_rate: 200 + } + }; + + const reportResponseB = await reportsRequestCreator.createReport(testIdB, minimalReportBodyB); + should(reportResponseB.statusCode).be.eql(201); + }); + + it('Post full cycle stats for both reports and export report', async function () { + const phaseStartedStatsResponseA = await reportsRequestCreator.postStats(testIdA, reportIdA, statsGenerator.generateStats('started_phase', runnerIdA)); + should(phaseStartedStatsResponseA.statusCode).be.eql(204); + + const getReportA = await reportsRequestCreator.getReport(testIdA, reportIdA); + should(getReportA.statusCode).be.eql(200); + const testStartTimeA = new Date(getReportA.body.start_time); + const statDateFirstA = new Date(testStartTimeA).setSeconds(testStartTimeA.getSeconds() + 20); + let intermediateStatsResponseA = await reportsRequestCreator.postStats(testIdA, reportIdA, statsGenerator.generateStats('intermediate', runnerIdA, statDateFirstA, 600)); + should(intermediateStatsResponseA.statusCode).be.eql(204); + + const statDateSecondA = new Date(testStartTimeA).setSeconds(testStartTimeA.getSeconds() + 40); + intermediateStatsResponseA = await reportsRequestCreator.postStats(testIdA, reportIdA, statsGenerator.generateStats('intermediate', runnerIdA, statDateSecondA, 200)); + should(intermediateStatsResponseA.statusCode).be.eql(204); + + const statDateThirdA = new Date(testStartTimeA).setSeconds(testStartTimeA.getSeconds() + 60); + const doneStatsResponseA = await reportsRequestCreator.postStats(testIdA, reportIdA, statsGenerator.generateStats('done', runnerIdA, statDateThirdA)); + should(doneStatsResponseA.statusCode).be.eql(204); + + const phaseStartedStatsResponseB = await reportsRequestCreator.postStats(testIdB, reportIdB, statsGenerator.generateStats('started_phase', runnerIdB)); + should(phaseStartedStatsResponseB.statusCode).be.eql(204); + + const getReportB = await reportsRequestCreator.getReport(testIdB, reportIdB); + should(getReportB.statusCode).be.eql(200); + const testStartTimeB = new Date(getReportB.body.start_time); + const statDateFirstB = new Date(testStartTimeB).setSeconds(testStartTimeB.getSeconds() + 20); + let intermediateStatsResponseB = await reportsRequestCreator.postStats(testIdB, reportIdB, statsGenerator.generateStats('intermediate', runnerIdB, statDateFirstB, 600)); + should(intermediateStatsResponseB.statusCode).be.eql(204); + + const statDateSecondB = new Date(testStartTimeB).setSeconds(testStartTimeB.getSeconds() + 40); + intermediateStatsResponseB = await reportsRequestCreator.postStats(testIdB, reportIdB, statsGenerator.generateStats('intermediate', runnerIdB, statDateSecondB, 200)); + should(intermediateStatsResponseB.statusCode).be.eql(204); + + const statDateThirdB = new Date(testStartTimeB).setSeconds(testStartTimeB.getSeconds() + 60); + const doneStatsResponseB = await reportsRequestCreator.postStats(testIdB, reportIdB, statsGenerator.generateStats('done', runnerIdB, statDateThirdB)); + should(doneStatsResponseB.statusCode).be.eql(204); + + const reportMetaData = { + test_ids: [testIdA, testIdB], + report_ids: [reportIdA, reportIdB] + }; + + const getExportedCompareResponse = await reportsRequestCreator.getExportedCompareReport('csv', reportMetaData); + should(getExportedCompareResponse.statusCode).be.eql(200); + const contentDisposition = new RegExp('attachment; filename=integration-test_integration-test_comparison_[a-f0-9\-]+_[a-f0-9\-]+\.csv'); + const contentType = new RegExp('text/csv; charset=utf-8'); + const receivedDisposition = getExportedCompareResponse.header['content-disposition']; + const receivedType = getExportedCompareResponse.header['content-type']; + + should(contentDisposition.test(receivedDisposition)).be.eql(true); + should(contentType.test(receivedType)).be.eql(true); + const EXPORTED_REPORT_HEADER = 'timestamp,timemillis,A_median,A_p95,A_p99,A_rps,A_status_200,B_median,B_p95,B_p99,B_rps,B_status_200'; + const reportLines = getExportedCompareResponse.text.split('\n'); + const headers = reportLines[0]; + should(headers).eql(EXPORTED_REPORT_HEADER); + }); + + it('Post full cycle stats for both reports and give an unknown report', async function () { + const phaseStartedStatsResponseA = await reportsRequestCreator.postStats(testIdA, reportIdA, statsGenerator.generateStats('started_phase', runnerIdA)); + should(phaseStartedStatsResponseA.statusCode).be.eql(204); + + const getReportA = await reportsRequestCreator.getReport(testIdA, reportIdA); + should(getReportA.statusCode).be.eql(200); + const testStartTimeA = new Date(getReportA.body.start_time); + const statDateFirstA = new Date(testStartTimeA).setSeconds(testStartTimeA.getSeconds() + 20); + let intermediateStatsResponseA = await reportsRequestCreator.postStats(testIdA, reportIdA, statsGenerator.generateStats('intermediate', runnerIdA, statDateFirstA, 600)); + should(intermediateStatsResponseA.statusCode).be.eql(204); + + const statDateSecondA = new Date(testStartTimeA).setSeconds(testStartTimeA.getSeconds() + 40); + intermediateStatsResponseA = await reportsRequestCreator.postStats(testIdA, reportIdA, statsGenerator.generateStats('intermediate', runnerIdA, statDateSecondA, 200)); + should(intermediateStatsResponseA.statusCode).be.eql(204); + + const statDateThirdA = new Date(testStartTimeA).setSeconds(testStartTimeA.getSeconds() + 60); + const doneStatsResponseA = await reportsRequestCreator.postStats(testIdA, reportIdA, statsGenerator.generateStats('done', runnerIdA, statDateThirdA)); + should(doneStatsResponseA.statusCode).be.eql(204); + + const phaseStartedStatsResponseB = await reportsRequestCreator.postStats(testIdB, reportIdB, statsGenerator.generateStats('started_phase', runnerIdB)); + should(phaseStartedStatsResponseB.statusCode).be.eql(204); + + const getReportB = await reportsRequestCreator.getReport(testIdB, reportIdB); + should(getReportB.statusCode).be.eql(200); + const testStartTimeB = new Date(getReportB.body.start_time); + const statDateFirstB = new Date(testStartTimeB).setSeconds(testStartTimeB.getSeconds() + 20); + let intermediateStatsResponseB = await reportsRequestCreator.postStats(testIdB, reportIdB, statsGenerator.generateStats('intermediate', runnerIdB, statDateFirstB, 600)); + should(intermediateStatsResponseB.statusCode).be.eql(204); + + const statDateSecondB = new Date(testStartTimeB).setSeconds(testStartTimeB.getSeconds() + 40); + intermediateStatsResponseB = await reportsRequestCreator.postStats(testIdB, reportIdB, statsGenerator.generateStats('intermediate', runnerIdB, statDateSecondB, 200)); + should(intermediateStatsResponseB.statusCode).be.eql(204); + + const statDateThirdB = new Date(testStartTimeB).setSeconds(testStartTimeB.getSeconds() + 60); + const doneStatsResponseB = await reportsRequestCreator.postStats(testIdB, reportIdB, statsGenerator.generateStats('done', runnerIdB, statDateThirdB)); + should(doneStatsResponseB.statusCode).be.eql(204); + + const reportMetaData = { + test_ids: [testIdA, testIdB], + report_ids: ['unknown', reportIdB] + }; + + const getExportedCompareResponse = await reportsRequestCreator.getExportedCompareReport('csv', reportMetaData); + should(getExportedCompareResponse.statusCode).be.eql(404); + }); + }); + describe('Post stats', function () { before(async function () { const jobResponse = await createJob(testId); diff --git a/ui/package-lock.json b/ui/package-lock.json index 61c947983..00ee5b583 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -14116,6 +14116,11 @@ "lodash": "^4.17.15" } }, + "webpack-node-externals": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-2.5.2.tgz", + "integrity": "sha512-aHdl/y2N7PW2Sx7K+r3AxpJO+aDMcYzMQd60Qxefq3+EwhewSbTBqNumOsCE1JsCUNoyfGj5465N0sSf6hc/5w==" + }, "webpack-sources": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", diff --git a/ui/src/features/components/Report/compareReports.js b/ui/src/features/components/Report/compareReports.js index 65bd1cc8b..04d428c10 100644 --- a/ui/src/features/components/Report/compareReports.js +++ b/ui/src/features/components/Report/compareReports.js @@ -85,6 +85,23 @@ class CompareReports extends React.Component { this.setState({filteredKeys: {...newFilteredKeys}}); }; + onClickExportCSV = () => { + const selectedReports = this.props.selectedReportsAsArray; + const reportsList = this.state.reportsList; + let request_string = ""; + let reportIdsAsCSV = ""; + let testIdsAsCSV = ""; + for (let index in selectedReports){ + reportIdsAsCSV+=selectedReports[index]["reportId"]; + testIdsAsCSV+=selectedReports[index]["testId"]; + if (index < selectedReports.length -1){ + reportIdsAsCSV+=","; + testIdsAsCSV+=","; + } + } + request_string = "report_ids="+reportIdsAsCSV+"&test_ids="+testIdsAsCSV; + window.open(`${process.env.PREDATOR_URL}/tests/reports/compare/export/csv?`+request_string,"_blank"); + }; render() { const {reportsList, mergedReports, filteredKeys, enableBenchmark} = this.state; @@ -98,6 +115,9 @@ class CompareReports extends React.Component { // alignItems: 'center' }}>