From 3dd58707271909daf080c5577a22e2e2159e0d1c Mon Sep 17 00:00:00 2001 From: Dan Homola Date: Sat, 15 Oct 2016 18:46:35 +0200 Subject: [PATCH] First working version Generates the trx file to a hardcoded location --- .gitignore | 4 ++ .npmignore | 3 + package.json | 28 ++++++++ src/index.ts | 184 ++++++++++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 10 +++ tslint.json | 58 ++++++++++++++++ yarn.lock | 163 ++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 450 insertions(+) create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 package.json create mode 100644 src/index.ts create mode 100644 tsconfig.json create mode 100644 tslint.json create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f0598f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +dist/ +node_modules/ +coverage/ +*.log diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..4c444c6 --- /dev/null +++ b/.npmignore @@ -0,0 +1,3 @@ +node_modules/ +coverage/ +*.log diff --git a/package.json b/package.json new file mode 100644 index 0000000..2e34a19 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "jest-trx-results-processor", + "version": "0.0.1", + "description": "Jest results processor for exporting into TRX files for Visual Studio", + "main": "dist/index.js", + "repository": { + "url": "https://github.com/no23reason/jest-trx-results-processor", + "type": "git" + }, + "author": "Dan Homola", + "license": "MIT", + "scripts": { + "prepublish": "npm run build", + "build": "tsc", + "test:tslint": "tslint --project ." + }, + "dependencies": { + "uuid": "~2.0.3", + "xmlbuilder": "~8.2.2" + }, + "devDependencies": { + "@types/node": "^6.0.45", + "@types/uuid": "^2.0.29", + "@types/xmlbuilder": "^0.0.28", + "tslint": "~3.15.1", + "typescript": "~2.0.3" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..d19d73f --- /dev/null +++ b/src/index.ts @@ -0,0 +1,184 @@ +import * as builder from "xmlbuilder"; +import * as uuid from "uuid"; +import * as os from "os"; +import { writeFileSync } from "fs"; + +type UnixTime = number; + +type JestTestResult = { + ancestorTitles: string[]; // array of messages in describe blocks + failureMessages: string[]; + numPassingAsserts: number; + status: "failed" | "pending" | "passed"; + title: string; // message in it block +} + +type JestTestSuiteResult = { + coverage: {}; + numFailingTests: number; + numPassingTests: number; + numPendingTests: number; + perfStats: { + start: UnixTime; + end: UnixTime; + }; + testFilePath: string; // absolute path to test file; + testResults: JestTestResult[]; +}; + +type JestTestRunResult = { + success: boolean; + startTime: UnixTime; + numTotalTestSuites: number; + numPassedTestSuites: number; + numFailedTestSuites: number; + numRuntimeErrorTestSuites: number; + numTotalTests: number; + numPassedTests: number; + numFailedTests: number; + numPendingTests: number; + testResults: JestTestSuiteResult[]; +} + +const testListNotInListId = "8c84fa94-04c1-424b-9868-57a2d4851a1d"; +const testListAllLoadedResultsId = "19431567-8539-422a-85d7-44ee4e166bda"; +const testType = "13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b"; + +const getFullTestName = (testResult: JestTestResult): string => + testResult.ancestorTitles && testResult.ancestorTitles.length + ? `${testResult.ancestorTitles.join(" / ")} / ${testResult.title}` + : testResult.title; + +const getTestClassName = (testResult: JestTestResult): string => + testResult.ancestorTitles && testResult.ancestorTitles.length + ? testResult.ancestorTitles[0] + : "No suite"; + +const getSuitePerTestDuration = (testSuiteResult: JestTestSuiteResult): number => + // take the total duration of suite and divide it by the number of tests (Jest does not provide per test info) + Math.floor((testSuiteResult.perfStats.end - testSuiteResult.perfStats.start) / (testSuiteResult.numPassingTests + testSuiteResult.numFailingTests)); + +const formatDuration = (duration: number): string => { + let durationInner = duration | 0; + const ms = durationInner % 1000; + durationInner -= ms; + const s = (durationInner / 1000) % 60; + durationInner -= s * 1000; + const m = (durationInner / 60000) % 60; + durationInner -= m * 60000; + const h = (durationInner / 3600000) % 24; + durationInner -= h * 3600000; + const d = durationInner / 86400000; + + return (d > 0 ? d + "." : "") + + (h < 10 ? "0" + h : h) + ":" + + (m < 10 ? "0" + m : m) + ":" + + (s < 10 ? "0" + s : s) + "." + + (ms < 10 ? "00" + ms : ms < 100 ? "0" + ms : ms); +}; + +const testOutcomeTable: {[outcome: string]: string} = { + "failed": "Failed", + "pending": "Skipped", + "passed": "Passed" +}; + +const processor = (testRunResult: JestTestRunResult): void => { + console.log("Generating TRX file..."); + const computerName = os.hostname(); + const userName = process.env.SUDO_USER || + process.env.LOGNAME || + process.env.USER || + process.env.LNAME || + process.env.USERNAME; + + // TestRun + const resultBuilder = builder.create("TestRun", { version: "1.0", encoding: "UTF-8" }) + .att("id", uuid.v4()) + .att("name", `${userName}@${computerName} ${new Date(testRunResult.startTime).toISOString()}`) + .att("runUser", userName) + .att("xmlns", "http://microsoft.com/schemas/VisualStudio/TeamTest/2010"); + // TestSettings + resultBuilder.ele("TestSettings") + .att("name", "Jest test run") + .att("id", uuid.v4()); + + // Times + const startTime = new Date(testRunResult.startTime).toISOString(); + resultBuilder.ele("Times") + .att("creation", startTime) + .att("queuing", startTime) + .att("start", startTime); + + // ResultSummary + const summary = resultBuilder.ele("ResultSummary") + .att("outcome", testRunResult.success ? "Passed" : "Failed"); + summary.ele("Counters") + .att("total", testRunResult.numTotalTests) + .att("executed", testRunResult.numTotalTests - testRunResult.numPendingTests) + .att("passed", testRunResult.numPassedTests) + .att("failed", testRunResult.numFailedTests) + .att("error", 0); + + // TestDefinitions + const testDefinitions = resultBuilder.ele("TestDefinitions"); + const testLists = resultBuilder.ele("TestLists"); + testLists.ele("TestList") + .att("name", "Results Not in a List") + .att("id", testListNotInListId); + testLists.ele("TestList") + .att("name", "All Loaded Results") + .att("id", testListAllLoadedResultsId); + const testEntries = resultBuilder.ele("TestEntries"); + const results = resultBuilder.ele("Results"); + + testRunResult.testResults.forEach(testSuiteResult => { + const perTestDuration = getSuitePerTestDuration(testSuiteResult); + const perTestDurationFormatted = formatDuration(perTestDuration); + testSuiteResult.testResults.forEach((testResult, index) => { + const testId = uuid.v4(); + const executionId = uuid.v4(); + const fullTestName = getFullTestName(testResult); + // UnitTest + const unitTest = testDefinitions.ele("UnitTest") + .att("name", fullTestName) + .att("id", testId); + unitTest.ele("Execution") + .att("id", executionId); + unitTest.ele("TestMethod") + .att("codeBase", `Jest_${fullTestName}`) + .att("name", fullTestName) + .att("className", getTestClassName(testResult)); + + // TestEntry + testEntries.ele("TestEntry") + .att("testId", testId) + .att("executionId", executionId) + .att("testListId", testListNotInListId); + + // UnitTestResult + const result = results.ele("UnitTestResult") + .att("testId", testId) + .att("executionId", executionId) + .att("testName", fullTestName) + .att("computerName", computerName) + .att("duration", perTestDuration) + .att("startTime", new Date(testSuiteResult.perfStats.start + index * perTestDuration).toISOString()) + .att("endTime", new Date(testSuiteResult.perfStats.start + (index + 1) * perTestDuration).toISOString()) + .att("testType", testType) + .att("outcome", testOutcomeTable[testResult.status]) + .att("testListId", testListNotInListId); + + if (testResult.status === "failed") { + result.ele("Output").ele("ErrorInfo").ele("Message", testResult.failureMessages.join("\n")); + } + }); + }); + + const xml = resultBuilder.end({ pretty: true }); + // console.log(xml); + writeFileSync("jest-test-results.trx", xml, { encoding: "utf8" }); + console.log("TRX file output to jest-test-results.trx"); +}; + +export = processor; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..cf8a08f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "noImplicitAny": true, + "sourceMap": false, + "strictNullChecks": true, + "outDir": "dist" + } +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..8a54876 --- /dev/null +++ b/tslint.json @@ -0,0 +1,58 @@ +{ + "rules": { + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "indent": [ + true, + "spaces" + ], + "no-duplicate-variable": true, + "no-eval": true, + "no-internal-module": true, + "no-trailing-whitespace": true, + "no-unsafe-finally": true, + "no-var-keyword": true, + "one-line": [ + true, + "check-open-brace", + "check-whitespace" + ], + "quotemark": [ + true, + "double" + ], + "semicolon": [ + true, + "always" + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "variable-name": [ + true, + "ban-keywords" + ], + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ] + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..24e3281 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,163 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 +"@types/node@*", "@types/node@^6.0.45": + version "6.0.45" + resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.45.tgz#c4842a9d653d767831e4ff495b6008cc0d579966" + +"@types/uuid@^2.0.29": + version "2.0.29" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-2.0.29.tgz#939a198ab73567f811ab84f670d2be9c25addd41" + dependencies: + "@types/node" "*" + +"@types/xmlbuilder@^0.0.28": + version "0.0.28" + resolved "https://registry.yarnpkg.com/@types/xmlbuilder/-/xmlbuilder-0.0.28.tgz#43f7535d24a9d43f5039b01836d3cdbd9ec971a6" + +balanced-match@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" + +brace-expansion@^1.0.0: + version "1.1.6" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9" + dependencies: + balanced-match "^0.4.1" + concat-map "0.0.1" + +colors@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +diff@^2.2.1: + version "2.2.3" + resolved "https://registry.yarnpkg.com/diff/-/diff-2.2.3.tgz#60eafd0d28ee906e4e8ff0a52c1229521033bf99" + +findup-sync@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.3.0.tgz#37930aa5d816b777c03445e1966cc6790a4c0b16" + dependencies: + glob "~5.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +glob@^7.0.3: + version "7.1.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.2" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@~5.0.0: + version "5.0.15" + resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +lodash.upperfirst@~4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz#1365edf431480481ef0d1c68957a5ed99d49f7ce" + +minimatch@^3.0.2, "minimatch@2 || 3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" + dependencies: + brace-expansion "^1.0.0" + +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +optimist@~0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +resolve@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + +sprintf-js@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +tslint@~3.15.1: + version "3.15.1" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-3.15.1.tgz#da165ca93d8fdc2c086b51165ee1bacb48c98ea5" + dependencies: + colors "^1.1.2" + diff "^2.2.1" + findup-sync "~0.3.0" + glob "^7.0.3" + optimist "~0.6.0" + resolve "^1.1.7" + underscore.string "^3.3.4" + +typescript@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.0.3.tgz#33dec9eae86b8eee327dd419ca050c853cabd514" + +underscore.string@^3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-3.3.4.tgz#2c2a3f9f83e64762fdc45e6ceac65142864213db" + dependencies: + sprintf-js "^1.0.3" + util-deprecate "^1.0.2" + +util-deprecate@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +uuid@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +xmlbuilder@~8.2.2: + version "8.2.2" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-8.2.2.tgz#69248673410b4ba42e1a6136551d2922335aa773" +