|  | 
|  | 1 | +// Copyright (c) Microsoft Corporation. All rights reserved. | 
|  | 2 | +// Licensed under the MIT License. | 
|  | 3 | + | 
|  | 4 | +'use strict'; | 
|  | 5 | + | 
|  | 6 | +import { inject, injectable } from 'inversify'; | 
|  | 7 | +import * as path from 'path'; | 
|  | 8 | +import { Uri } from 'vscode'; | 
|  | 9 | +import { IWorkspaceService } from '../../../common/application/types'; | 
|  | 10 | +import { traceError } from '../../../common/logger'; | 
|  | 11 | +import { IFileSystem } from '../../../common/platform/types'; | 
|  | 12 | +import { TestDataItem } from '../../types'; | 
|  | 13 | +import { getParentFile, getParentSuite, getTestType } from '../testUtils'; | 
|  | 14 | +import { FlattenedTestFunction, FlattenedTestSuite, SubtestParent, TestFile, TestFolder, TestFunction, Tests, TestSuite, TestType } from '../types'; | 
|  | 15 | +import { DiscoveredTests, ITestDiscoveredTestParser, TestContainer, TestItem } from './types'; | 
|  | 16 | + | 
|  | 17 | +@injectable() | 
|  | 18 | +export class TestDiscoveredTestParser implements ITestDiscoveredTestParser { | 
|  | 19 | + constructor(@inject(IFileSystem) private readonly fs: IFileSystem, | 
|  | 20 | + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService) { } | 
|  | 21 | + public parse(resource: Uri, discoveredTests: DiscoveredTests[]): Tests { | 
|  | 22 | + const tests: Tests = { | 
|  | 23 | + rootTestFolders: [], | 
|  | 24 | + summary: { errors: 0, failures: 0, passed: 0, skipped: 0 }, | 
|  | 25 | + testFiles: [], | 
|  | 26 | + testFolders: [], | 
|  | 27 | + testFunctions: [], | 
|  | 28 | + testSuites: [] | 
|  | 29 | + }; | 
|  | 30 | + | 
|  | 31 | + const workspace = this.workspaceService.getWorkspaceFolder(resource); | 
|  | 32 | + if (!workspace) { | 
|  | 33 | + traceError('Resource does not belong to any workspace folder'); | 
|  | 34 | + return tests; | 
|  | 35 | + } | 
|  | 36 | + | 
|  | 37 | + // If the root is the workspace folder, then ignore that. | 
|  | 38 | + for (const data of discoveredTests) { | 
|  | 39 | + const rootFolder = { | 
|  | 40 | + name: data.root, folders: [], time: 0, | 
|  | 41 | + testFiles: [], resource: resource, nameToRun: data.rootid | 
|  | 42 | + }; | 
|  | 43 | + tests.rootTestFolders.push(rootFolder); | 
|  | 44 | + tests.testFolders.push(rootFolder); | 
|  | 45 | + this.buildChildren(rootFolder, rootFolder, data, tests); | 
|  | 46 | + this.fixRootFolders(workspace.uri, data, tests); | 
|  | 47 | + } | 
|  | 48 | + | 
|  | 49 | + return tests; | 
|  | 50 | + } | 
|  | 51 | + /** | 
|  | 52 | + * Users workspace folder is not to be treated as the root. | 
|  | 53 | + * All root folders are relative to the worspace folder. | 
|  | 54 | + * @protected | 
|  | 55 | + * @param {Uri} workspaceFolder | 
|  | 56 | + * @param {DiscoveredTests} discoveredTests | 
|  | 57 | + * @param {Tests} tests | 
|  | 58 | + * @returns {void} | 
|  | 59 | + * @memberof TestDiscoveredTestParser | 
|  | 60 | + */ | 
|  | 61 | + protected fixRootFolders(workspaceFolder: Uri, discoveredTests: DiscoveredTests, tests: Tests): void { | 
|  | 62 | + const isWorkspaceFolderTheRoot = this.fs.arePathsSame(discoveredTests.root, workspaceFolder.fsPath); | 
|  | 63 | + if (!isWorkspaceFolderTheRoot) { | 
|  | 64 | + return; | 
|  | 65 | + } | 
|  | 66 | + const indexToRemove = tests.rootTestFolders.findIndex(folder => folder.name === discoveredTests.root); | 
|  | 67 | + const rootFolder = tests.rootTestFolders.splice(indexToRemove, 1)[0]; | 
|  | 68 | + tests.rootTestFolders.push(...rootFolder.folders); | 
|  | 69 | + } | 
|  | 70 | + /** | 
|  | 71 | + * Not the best solution to use `case statements`, but it keeps the code simple and easy to read in one place. | 
|  | 72 | + * Could go with separate classes for each type and use stratergies, but that just ends up a class for | 
|  | 73 | + * 10 lines of code. Hopefully this is more readable and maintainable than having multiple classes for | 
|  | 74 | + * the simple processing of the children. | 
|  | 75 | + * | 
|  | 76 | + * @protected | 
|  | 77 | + * @param {TestFolder} rootFolder | 
|  | 78 | + * @param {TestDataItem} parent | 
|  | 79 | + * @param {DiscoveredTests} discoveredTests | 
|  | 80 | + * @param {Tests} tests | 
|  | 81 | + * @memberof TestsDiscovery | 
|  | 82 | + */ | 
|  | 83 | + protected buildChildren(rootFolder: TestFolder, parent: TestDataItem, discoveredTests: DiscoveredTests, tests: Tests) { | 
|  | 84 | + const parentType = getTestType(parent); | 
|  | 85 | + switch (parentType) { | 
|  | 86 | + case TestType.testFolder: { | 
|  | 87 | + this.processFolder(rootFolder, parent as TestFolder, discoveredTests, tests); | 
|  | 88 | + break; | 
|  | 89 | + } | 
|  | 90 | + case TestType.testFile: { | 
|  | 91 | + this.processFile(rootFolder, parent as TestFile, discoveredTests, tests); | 
|  | 92 | + break; | 
|  | 93 | + } | 
|  | 94 | + case TestType.testSuite: { | 
|  | 95 | + this.processSuite(rootFolder, parent as TestSuite, discoveredTests, tests); | 
|  | 96 | + break; | 
|  | 97 | + } | 
|  | 98 | + default: | 
|  | 99 | + break; | 
|  | 100 | + } | 
|  | 101 | + } | 
|  | 102 | + /** | 
|  | 103 | + * Process the children of a folder. | 
|  | 104 | + * A folder can only contain other folders and files. | 
|  | 105 | + * Hence limit processing to those items. | 
|  | 106 | + * | 
|  | 107 | + * @protected | 
|  | 108 | + * @param {TestFolder} rootFolder | 
|  | 109 | + * @param {TestFolder} parentFolder | 
|  | 110 | + * @param {DiscoveredTests} discoveredTests | 
|  | 111 | + * @param {Tests} tests | 
|  | 112 | + * @memberof TestDiscoveredTestParser | 
|  | 113 | + */ | 
|  | 114 | + protected processFolder(rootFolder: TestFolder, parentFolder: TestFolder, discoveredTests: DiscoveredTests, tests: Tests) { | 
|  | 115 | + const folders = discoveredTests.parents | 
|  | 116 | + .filter(child => child.kind === 'folder' && child.parentid === parentFolder.nameToRun) | 
|  | 117 | + .map(folder => createTestFolder(rootFolder, folder)); | 
|  | 118 | + | 
|  | 119 | + const files = discoveredTests.parents | 
|  | 120 | + .filter(child => child.kind === 'file' && child.parentid === parentFolder.nameToRun) | 
|  | 121 | + .map(file => createTestFile(rootFolder, file)); | 
|  | 122 | + | 
|  | 123 | + parentFolder.folders.push(...folders); | 
|  | 124 | + parentFolder.testFiles.push(...files); | 
|  | 125 | + tests.testFolders.push(...folders); | 
|  | 126 | + tests.testFiles.push(...files); | 
|  | 127 | + [...folders, ...files].forEach(item => this.buildChildren(rootFolder, item, discoveredTests, tests)); | 
|  | 128 | + } | 
|  | 129 | + /** | 
|  | 130 | + * Process the children of a file. | 
|  | 131 | + * A file can only contain suites, functions and paramerterized functions. | 
|  | 132 | + * Hence limit processing just to those items. | 
|  | 133 | + * | 
|  | 134 | + * @protected | 
|  | 135 | + * @param {TestFolder} rootFolder | 
|  | 136 | + * @param {TestFile} parentFile | 
|  | 137 | + * @param {DiscoveredTests} discoveredTests | 
|  | 138 | + * @param {Tests} tests | 
|  | 139 | + * @memberof TestDiscoveredTestParser | 
|  | 140 | + */ | 
|  | 141 | + protected processFile(rootFolder: TestFolder, parentFile: TestFile, discoveredTests: DiscoveredTests, tests: Tests) { | 
|  | 142 | + const suites = discoveredTests.parents | 
|  | 143 | + .filter(child => child.kind === 'suite' && child.parentid === parentFile.nameToRun) | 
|  | 144 | + .map(suite => createTestSuite(parentFile, rootFolder.resource, suite)); | 
|  | 145 | + | 
|  | 146 | + const functions = discoveredTests.tests | 
|  | 147 | + .filter(func => func.parentid === parentFile.nameToRun) | 
|  | 148 | + .map(func => createTestFunction(rootFolder, func)); | 
|  | 149 | + | 
|  | 150 | + parentFile.suites.push(...suites); | 
|  | 151 | + parentFile.functions.push(...functions); | 
|  | 152 | + tests.testSuites.push(...suites.map(suite => createFlattenedSuite(tests, suite))); | 
|  | 153 | + tests.testFunctions.push(...functions.map(func => createFlattenedFunction(tests, func))); | 
|  | 154 | + suites.forEach(item => this.buildChildren(rootFolder, item, discoveredTests, tests)); | 
|  | 155 | + | 
|  | 156 | + const parameterizedFunctions = discoveredTests.parents | 
|  | 157 | + .filter(child => child.kind === 'function' && child.parentid === parentFile.nameToRun) | 
|  | 158 | + .map(func => createParameterizedTestFunction(rootFolder, func)); | 
|  | 159 | + parameterizedFunctions.forEach(func => this.processParameterizedFunction(rootFolder, parentFile, func, discoveredTests, tests)); | 
|  | 160 | + } | 
|  | 161 | + /** | 
|  | 162 | + * Process the children of a suite. | 
|  | 163 | + * A suite can only contain suites, functions and paramerterized functions. | 
|  | 164 | + * Hence limit processing just to those items. | 
|  | 165 | + * | 
|  | 166 | + * @protected | 
|  | 167 | + * @param {TestFolder} rootFolder | 
|  | 168 | + * @param {TestSuite} parentSuite | 
|  | 169 | + * @param {DiscoveredTests} discoveredTests | 
|  | 170 | + * @param {Tests} tests | 
|  | 171 | + * @memberof TestDiscoveredTestParser | 
|  | 172 | + */ | 
|  | 173 | + protected processSuite(rootFolder: TestFolder, parentSuite: TestSuite, discoveredTests: DiscoveredTests, tests: Tests) { | 
|  | 174 | + const suites = discoveredTests.parents | 
|  | 175 | + .filter(child => child.kind === 'suite' && child.parentid === parentSuite.nameToRun) | 
|  | 176 | + .map(suite => createTestSuite(parentSuite, rootFolder.resource, suite)); | 
|  | 177 | + | 
|  | 178 | + const functions = discoveredTests.tests | 
|  | 179 | + .filter(func => func.parentid === parentSuite.nameToRun) | 
|  | 180 | + .map(func => createTestFunction(rootFolder, func)); | 
|  | 181 | + | 
|  | 182 | + parentSuite.suites.push(...suites); | 
|  | 183 | + parentSuite.functions.push(...functions); | 
|  | 184 | + tests.testSuites.push(...suites.map(suite => createFlattenedSuite(tests, suite))); | 
|  | 185 | + tests.testFunctions.push(...functions.map(func => createFlattenedFunction(tests, func))); | 
|  | 186 | + suites.forEach(item => this.buildChildren(rootFolder, item, discoveredTests, tests)); | 
|  | 187 | + | 
|  | 188 | + const parameterizedFunctions = discoveredTests.parents | 
|  | 189 | + .filter(child => child.kind === 'function' && child.parentid === parentSuite.nameToRun) | 
|  | 190 | + .map(func => createParameterizedTestFunction(rootFolder, func)); | 
|  | 191 | + parameterizedFunctions.forEach(func => this.processParameterizedFunction(rootFolder, parentSuite, func, discoveredTests, tests)); | 
|  | 192 | + } | 
|  | 193 | + /** | 
|  | 194 | + * Process the children of a parameterized function. | 
|  | 195 | + * A parameterized function can only contain functions (in tests). | 
|  | 196 | + * Hence limit processing just to those items. | 
|  | 197 | + * | 
|  | 198 | + * @protected | 
|  | 199 | + * @param {TestFolder} rootFolder | 
|  | 200 | + * @param {TestFunction} parentFunction | 
|  | 201 | + * @param {DiscoveredTests} discoveredTests | 
|  | 202 | + * @param {Tests} tests | 
|  | 203 | + * @returns | 
|  | 204 | + * @memberof TestDiscoveredTestParser | 
|  | 205 | + */ | 
|  | 206 | + protected processParameterizedFunction(rootFolder: TestFolder, parent: TestFile | TestSuite, parentFunction: SubtestParent, discoveredTests: DiscoveredTests, tests: Tests) { | 
|  | 207 | + if (!parentFunction.asSuite) { | 
|  | 208 | + return; | 
|  | 209 | + } | 
|  | 210 | + const functions = discoveredTests.tests | 
|  | 211 | + .filter(func => func.parentid === parentFunction.nameToRun) | 
|  | 212 | + .map(func => createTestFunction(rootFolder, func)); | 
|  | 213 | + functions.map(func => func.subtestParent = parentFunction); | 
|  | 214 | + parentFunction.asSuite.functions.push(...functions); | 
|  | 215 | + parent.functions.push(...functions); | 
|  | 216 | + tests.testFunctions.push(...functions.map(func => createFlattenedParameterizedFunction(tests, func, parent))); | 
|  | 217 | + } | 
|  | 218 | +} | 
|  | 219 | + | 
|  | 220 | +function createTestFolder(root: TestFolder, item: TestContainer): TestFolder { | 
|  | 221 | + return { | 
|  | 222 | + name: item.name, nameToRun: item.id, resource: root.resource, time: 0, folders: [], testFiles: [] | 
|  | 223 | + }; | 
|  | 224 | +} | 
|  | 225 | +function createTestFile(root: TestFolder, item: TestContainer): TestFile { | 
|  | 226 | + const fullyQualifiedName = path.isAbsolute(item.id) ? item.id : path.resolve(root.name, item.id); | 
|  | 227 | + return { | 
|  | 228 | + fullPath: fullyQualifiedName, functions: [], name: item.name, | 
|  | 229 | + nameToRun: item.id, resource: root.resource, suites: [], time: 0, xmlName: createXmlName(item.id) | 
|  | 230 | + }; | 
|  | 231 | +} | 
|  | 232 | +function createTestSuite(parentSuiteFile: TestFile | TestSuite, resource: Uri, item: TestContainer): TestSuite { | 
|  | 233 | + const suite = { | 
|  | 234 | + functions: [], name: item.name, nameToRun: item.id, resource: resource, | 
|  | 235 | + suites: [], time: 0, xmlName: '', isInstance: false, isUnitTest: false | 
|  | 236 | + }; | 
|  | 237 | + suite.xmlName = `${parentSuiteFile.xmlName}.${item.name}`; | 
|  | 238 | + return suite; | 
|  | 239 | +} | 
|  | 240 | +function createFlattenedSuite(tests: Tests, suite: TestSuite): FlattenedTestSuite { | 
|  | 241 | + const parentFile = getParentFile(tests, suite); | 
|  | 242 | + return { | 
|  | 243 | + parentTestFile: parentFile, testSuite: suite, xmlClassName: parentFile.xmlName | 
|  | 244 | + }; | 
|  | 245 | +} | 
|  | 246 | +function createFlattenedParameterizedFunction(tests: Tests, func: TestFunction, parent: TestFile | TestSuite): FlattenedTestFunction { | 
|  | 247 | + const type = getTestType(parent); | 
|  | 248 | + const parentFile = (type && type === TestType.testSuite) ? getParentFile(tests, func) : parent as TestFile; | 
|  | 249 | + const parentSuite = (type && type === TestType.testSuite) ? parent as TestSuite : undefined; | 
|  | 250 | + return { | 
|  | 251 | + parentTestFile: parentFile, parentTestSuite: parentSuite, | 
|  | 252 | + xmlClassName: parentSuite ? parentSuite.xmlName : parentFile.xmlName, testFunction: func | 
|  | 253 | + }; | 
|  | 254 | +} | 
|  | 255 | +function createFlattenedFunction(tests: Tests, func: TestFunction): FlattenedTestFunction { | 
|  | 256 | + const parent = getParentFile(tests, func); | 
|  | 257 | + const type = parent ? getTestType(parent) : undefined; | 
|  | 258 | + const parentFile = (type && type === TestType.testSuite) ? getParentFile(tests, func) : parent as TestFile; | 
|  | 259 | + const parentSuite = getParentSuite(tests, func); | 
|  | 260 | + return { | 
|  | 261 | + parentTestFile: parentFile, parentTestSuite: parentSuite, | 
|  | 262 | + xmlClassName: parentSuite ? parentSuite.xmlName : parentFile.xmlName, testFunction: func | 
|  | 263 | + }; | 
|  | 264 | +} | 
|  | 265 | +function createParameterizedTestFunction(root: TestFolder, item: TestContainer): SubtestParent { | 
|  | 266 | + const suite: TestSuite = { | 
|  | 267 | + functions: [], isInstance: false, isUnitTest: false, | 
|  | 268 | + name: item.name, nameToRun: item.id, resource: root.resource, | 
|  | 269 | + time: 0, suites: [], xmlName: '' | 
|  | 270 | + }; | 
|  | 271 | + return { | 
|  | 272 | + asSuite: suite, name: item.name, nameToRun: item.id, time: 0 | 
|  | 273 | + }; | 
|  | 274 | +} | 
|  | 275 | +function createTestFunction(root: TestFolder, item: TestItem): TestFunction { | 
|  | 276 | + return { | 
|  | 277 | + name: item.name, nameToRun: item.id, resource: root.resource, | 
|  | 278 | + time: 0, file: item.source.substr(0, item.source.lastIndexOf(':')) | 
|  | 279 | + }; | 
|  | 280 | +} | 
|  | 281 | +/** | 
|  | 282 | + * Creates something known as an Xml Name, used to identify items | 
|  | 283 | + * from an xunit test result. | 
|  | 284 | + * Once we have the test runner done in Python, this can be discarded. | 
|  | 285 | + * @param {string} fileId | 
|  | 286 | + * @returns | 
|  | 287 | + */ | 
|  | 288 | +function createXmlName(fileId: string) { | 
|  | 289 | + let name = path.join(path.dirname(fileId), path.basename(fileId, path.extname(fileId))); | 
|  | 290 | + name = name.replace(/\\/g, '.').replace(/\//g, '.'); | 
|  | 291 | + // Remove leading . & / & \ | 
|  | 292 | + while (name.startsWith('.') || name.startsWith('/') || name.startsWith('\\')) { | 
|  | 293 | + name = name.substring(1); | 
|  | 294 | + } | 
|  | 295 | + return name; | 
|  | 296 | +} | 
0 commit comments