diff --git a/src/client/testing/common/services/discoveredTestParser.ts b/src/client/testing/common/services/discoveredTestParser.ts index 308ce17c5507..971dc25f789a 100644 --- a/src/client/testing/common/services/discoveredTestParser.ts +++ b/src/client/testing/common/services/discoveredTestParser.ts @@ -8,8 +8,8 @@ import * as path from 'path'; import { Uri } from 'vscode'; import { IWorkspaceService } from '../../../common/application/types'; import { traceError } from '../../../common/logger'; -import { TestDataItem } from '../../types'; -import { getParentFile, getParentSuite, getTestType } from '../testUtils'; +import { TestDataItem, TestDataItemType } from '../../types'; +import { getParentFile, getParentSuite, getTestDataItemType } from '../testUtils'; import * as testing from '../types'; import * as discovery from './types'; @@ -74,17 +74,17 @@ export class TestDiscoveredTestParser implements discovery.ITestDiscoveredTestPa discoveredTests: discovery.DiscoveredTests, tests: testing.Tests ) { - const parentType = getTestType(parent); + const parentType = getTestDataItemType(parent); switch (parentType) { - case testing.TestType.testFolder: { + case TestDataItemType.folder: { this.processFolder(rootFolder, parent as testing.TestFolder, discoveredTests, tests); break; } - case testing.TestType.testFile: { + case TestDataItemType.file: { this.processFile(rootFolder, parent as testing.TestFile, discoveredTests, tests); break; } - case testing.TestType.testSuite: { + case TestDataItemType.suite: { this.processSuite(rootFolder, parent as testing.TestSuite, discoveredTests, tests); break; } @@ -383,11 +383,11 @@ function createFlattenedParameterizedFunction( func: testing.TestFunction, parent: testing.TestFile | testing.TestSuite ): testing.FlattenedTestFunction { - const type = getTestType(parent); - const parentFile = (type && type === testing.TestType.testSuite) ? + const type = getTestDataItemType(parent); + const parentFile = (type && type === TestDataItemType.suite) ? getParentFile(tests, func) : parent as testing.TestFile; - const parentSuite = (type && type === testing.TestType.testSuite) ? + const parentSuite = (type && type === TestDataItemType.suite) ? parent as testing.TestSuite : undefined; return { @@ -403,8 +403,8 @@ function createFlattenedFunction( func: testing.TestFunction ): testing.FlattenedTestFunction { const parent = getParentFile(tests, func); - const type = parent ? getTestType(parent) : undefined; - const parentFile = (type && type === testing.TestType.testSuite) ? + const type = parent ? getTestDataItemType(parent) : undefined; + const parentFile = (type && type === TestDataItemType.suite) ? getParentFile(tests, func) : parent as testing.TestFile; const parentSuite = getParentSuite(tests, func); diff --git a/src/client/testing/common/services/testResultsService.ts b/src/client/testing/common/services/testResultsService.ts index caf9fa586009..a0e9edc9982d 100644 --- a/src/client/testing/common/services/testResultsService.ts +++ b/src/client/testing/common/services/testResultsService.ts @@ -1,7 +1,7 @@ import { inject, injectable, named } from 'inversify'; -import { TestDataItem } from '../../types'; -import { getChildren, getTestType } from '../testUtils'; -import { ITestResultsService, ITestVisitor, Tests, TestStatus, TestType } from './../types'; +import { TestDataItem, TestDataItemType } from '../../types'; +import { getChildren, getTestDataItemType } from '../testUtils'; +import { ITestResultsService, ITestVisitor, Tests, TestStatus } from '../types'; @injectable() export class TestResultsService implements ITestResultsService { @@ -33,7 +33,7 @@ export class TestResultsService implements ITestResultsService { } } private updateTestItem(test: TestDataItem): void { - if (getTestType(test) === TestType.testFunction) { + if (getTestDataItemType(test) === TestDataItemType.function) { return; } let allChildrenPassed = true; @@ -42,7 +42,7 @@ export class TestResultsService implements ITestResultsService { const children = getChildren(test); children.forEach(child => { - if (getTestType(child) === TestType.testFunction) { + if (getTestDataItemType(child) === TestDataItemType.function) { if (typeof child.passed === 'boolean') { noChildrenRan = false; if (child.passed) { diff --git a/src/client/testing/common/testUtils.ts b/src/client/testing/common/testUtils.ts index ed5b1cc403af..185300838336 100644 --- a/src/client/testing/common/testUtils.ts +++ b/src/client/testing/common/testUtils.ts @@ -5,7 +5,7 @@ import { IApplicationShell, ICommandManager } from '../../common/application/typ import * as constants from '../../common/constants'; import { ITestingSettings, Product } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; -import { TestDataItem, TestWorkspaceFolder } from '../types'; +import { TestDataItem, TestDataItemType, TestWorkspaceFolder } from '../types'; import { CommandSource } from './constants'; import { TestFlatteningVisitor } from './testVisitors/flatteningVisitor'; import { @@ -21,7 +21,6 @@ import { TestSettingsPropertyNames, TestsToRun, TestSuite, - TestType, UnitTestProduct } from './types'; @@ -244,21 +243,21 @@ export class TestsHelper implements ITestsHelper { } } -export function getTestType(test: TestDataItem): TestType { +export function getTestDataItemType(test: TestDataItem): TestDataItemType { if (test instanceof TestWorkspaceFolder) { - return TestType.testWorkspaceFolder; + return TestDataItemType.workspaceFolder; } if (getTestFile(test)) { - return TestType.testFile; + return TestDataItemType.file; } if (getTestFolder(test)) { - return TestType.testFolder; + return TestDataItemType.folder; } if (getTestSuite(test)) { - return TestType.testSuite; + return TestDataItemType.suite; } if (getTestFunction(test)) { - return TestType.testFunction; + return TestDataItemType.function; } throw new Error('Unknown test type'); } @@ -305,14 +304,14 @@ export function getTestFunction(test: TestDataItem): TestFunction | undefined { * @returns {(TestDataItem | undefined)} */ export function getParent(tests: Tests, data: TestDataItem): TestDataItem | undefined { - switch (getTestType(data)) { - case TestType.testFile: { + switch (getTestDataItemType(data)) { + case TestDataItemType.file: { return getParentTestFolderForFile(tests, data as TestFile); } - case TestType.testFolder: { + case TestDataItemType.folder: { return getParentTestFolder(tests, data as TestFolder); } - case TestType.testSuite: { + case TestDataItemType.suite: { const suite = data as TestSuite; if (isSubtestsParent(suite)) { const fn = suite.functions[0]; @@ -328,7 +327,7 @@ export function getParent(tests: Tests, data: TestDataItem): TestDataItem | unde } return tests.testFiles.find(item => item.suites.indexOf(suite) >= 0); } - case TestType.testFunction: { + case TestDataItemType.function: { const fn = data as TestFunction; if (fn.subtestParent) { return fn.subtestParent.asSuite; @@ -354,7 +353,7 @@ export function getParent(tests: Tests, data: TestDataItem): TestDataItem | unde * @returns {(TestFolder | undefined)} */ function getParentTestFolder(tests: Tests, item: TestFolder | TestFile): TestFolder | undefined { - if (getTestType(item) === TestType.testFolder) { + if (getTestDataItemType(item) === TestDataItemType.folder) { return getParentTestFolderForFolder(tests, item as TestFolder); } return getParentTestFolderForFile(tests, item as TestFile); @@ -370,7 +369,7 @@ function getParentTestFolder(tests: Tests, item: TestFolder | TestFile): TestFol export function getParentFile(tests: Tests, suite: TestSuite | TestFunction): TestFile { let parent = getParent(tests, suite); while (parent) { - if (getTestType(parent) === TestType.testFile) { + if (getTestDataItemType(parent) === TestDataItemType.file) { return parent as TestFile; } parent = getParent(tests, parent); @@ -387,7 +386,7 @@ export function getParentFile(tests: Tests, suite: TestSuite | TestFunction): Te export function getParentSuite(tests: Tests, suite: TestSuite | TestFunction): TestSuite | undefined { let parent = getParent(tests, suite); while (parent) { - if (getTestType(parent) === TestType.testSuite) { + if (getTestDataItemType(parent) === TestDataItemType.suite) { return parent as TestSuite; } parent = getParent(tests, parent); @@ -453,14 +452,14 @@ export function findFlattendTestSuite(tests: Tests, suite: TestSuite): Flattened * @returns {TestDataItem[]} */ export function getChildren(item: TestDataItem): TestDataItem[] { - switch (getTestType(item)) { - case TestType.testFolder: { + switch (getTestDataItemType(item)) { + case TestDataItemType.folder: { return [ ...(item as TestFolder).folders, ...(item as TestFolder).testFiles ]; } - case TestType.testFile: { + case TestDataItemType.file: { const [subSuites, functions] = divideSubtests((item as TestFile).functions); return [ ...functions, @@ -468,7 +467,7 @@ export function getChildren(item: TestDataItem): TestDataItem[] { ...subSuites ]; } - case TestType.testSuite: { + case TestDataItemType.suite: { let subSuites: TestSuite[] = []; let functions = (item as TestSuite).functions; if (!isSubtestsParent((item as TestSuite))) { @@ -480,7 +479,7 @@ export function getChildren(item: TestDataItem): TestDataItem[] { ...subSuites ]; } - case TestType.testFunction: { + case TestDataItemType.function: { return []; } default: { diff --git a/src/client/testing/common/types.ts b/src/client/testing/common/types.ts index 4723c60ff4ce..1e02fe6eb970 100644 --- a/src/client/testing/common/types.ts +++ b/src/client/testing/common/types.ts @@ -10,6 +10,14 @@ import { CommandSource } from './constants'; export type TestProvider = 'nosetest' | 'pytest' | 'unittest'; +export type UnitTestProduct = Product.nosetest | Product.pytest | Product.unittest; + +export type TestSettingsPropertyNames = { + enabledName: keyof ITestingSettings; + argsName: keyof ITestingSettings; + pathName?: keyof ITestingSettings; +}; + export type TestDiscoveryOptions = { workspaceFolder: Uri; cwd: string; @@ -32,46 +40,95 @@ export type TestRunOptions = { export type UnitTestParserOptions = TestDiscoveryOptions & { startDirectory: string }; -export type TestFolder = TestResult & { - resource: Uri; +export type LaunchOptions = { + cwd: string; + args: string[]; + testProvider: TestProvider; + token?: CancellationToken; + outChannel?: OutputChannel; +}; + +export type ParserOptions = TestDiscoveryOptions; + +export type Options = { + workspaceFolder: Uri; + cwd: string; + args: string[]; + outChannel?: OutputChannel; + token: CancellationToken; +}; + +export type TestsToRun = { + testFolder?: TestFolder[]; + testFile?: TestFile[]; + testSuite?: TestSuite[]; + testFunction?: TestFunction[]; +}; + +//***************** +// test results + +export enum TestingType { + folder = 'folder', + file = 'file', + suite = 'suite', + function = 'function' +} + +export enum TestStatus { + Unknown = 'Unknown', + Discovering = 'Discovering', + Idle = 'Idle', + Running = 'Running', + Fail = 'Fail', + Error = 'Error', + Skipped = 'Skipped', + Pass = 'Pass' +} + +export type TestResult = { + status?: TestStatus; + passed?: boolean; + time: number; + line?: number; + file?: string; + message?: string; + traceback?: string; + functionsPassed?: number; + functionsFailed?: number; + functionsDidNotRun?: number; +}; + +export type TestingNode = TestResult & { name: string; - testFiles: TestFile[]; nameToRun: string; + resource: Uri; +}; + +export type TestFolder = TestingNode & { folders: TestFolder[]; + testFiles: TestFile[]; }; -export enum TestType { - testFile = 'testFile', - testFolder = 'testFolder', - testSuite = 'testSuite', - testFunction = 'testFunction', - testWorkspaceFolder = 'testWorkspaceFolder' -} -export type TestFile = TestResult & { - resource: Uri; - name: string; + +export type TestingXMLNode = TestingNode & { + xmlName: string; +}; + +export type TestFile = TestingXMLNode & { fullPath: string; functions: TestFunction[]; suites: TestSuite[]; - nameToRun: string; - xmlName: string; errorsWhenDiscovering?: string; }; -export type TestSuite = TestResult & { - resource: Uri; - name: string; +export type TestSuite = TestingXMLNode & { functions: TestFunction[]; suites: TestSuite[]; isUnitTest: Boolean; isInstance: Boolean; - nameToRun: string; - xmlName: string; }; -export type TestFunction = TestResult & { - resource: Uri; - name: string; - nameToRun: string; +export type TestFunction = TestingNode & { subtestParent?: SubtestParent; }; @@ -81,23 +138,6 @@ export type SubtestParent = TestResult & { asSuite: TestSuite; }; -export type TestResult = Node & { - status?: TestStatus; - passed?: boolean; - time: number; - line?: number; - file?: string; - message?: string; - traceback?: string; - functionsPassed?: number; - functionsFailed?: number; - functionsDidNotRun?: number; -}; - -export type Node = { - expanded?: Boolean; -}; - export type FlattenedTestFunction = { testFunction: TestFunction; parentTestSuite?: TestSuite; @@ -127,25 +167,8 @@ export type Tests = { rootTestFolders: TestFolder[]; }; -export enum TestStatus { - Unknown = 'Unknown', - Discovering = 'Discovering', - Idle = 'Idle', - Running = 'Running', - Fail = 'Fail', - Error = 'Error', - Skipped = 'Skipped', - Pass = 'Pass' -} - -export type TestsToRun = { - testFolder?: TestFolder[]; - testFile?: TestFile[]; - testSuite?: TestSuite[]; - testFunction?: TestFunction[]; -}; - -export type UnitTestProduct = Product.nosetest | Product.pytest | Product.unittest; +//***************** +// interfaces export interface ITestManagerService extends Disposable { getTestManager(): ITestManager | undefined; @@ -154,21 +177,13 @@ export interface ITestManagerService extends Disposable { } export const IWorkspaceTestManagerService = Symbol('IWorkspaceTestManagerService'); - export interface IWorkspaceTestManagerService extends Disposable { getTestManager(resource: Uri): ITestManager | undefined; getTestWorkingDirectory(resource: Uri): string; getPreferredTestManager(resource: Uri): UnitTestProduct | undefined; } -export type TestSettingsPropertyNames = { - enabledName: keyof ITestingSettings; - argsName: keyof ITestingSettings; - pathName?: keyof ITestingSettings; -}; - export const ITestsHelper = Symbol('ITestsHelper'); - export interface ITestsHelper { parseProviderName(product: UnitTestProduct): TestProvider; parseProduct(provider: TestProvider): UnitTestProduct; @@ -181,7 +196,6 @@ export interface ITestsHelper { } export const ITestVisitor = Symbol('ITestVisitor'); - export interface ITestVisitor { visitTestFunction(testFunction: TestFunction): void; visitTestSuite(testSuite: TestSuite): void; @@ -190,7 +204,6 @@ export interface ITestVisitor { } export const ITestCollectionStorageService = Symbol('ITestCollectionStorageService'); - export interface ITestCollectionStorageService extends Disposable { onDidChange: Event<{ uri: Uri; data?: TestDataItem }>; getTests(wkspace: Uri): Tests | undefined; @@ -201,34 +214,23 @@ export interface ITestCollectionStorageService extends Disposable { } export const ITestResultsService = Symbol('ITestResultsService'); - export interface ITestResultsService { resetResults(tests: Tests): void; updateResults(tests: Tests): void; } -export type LaunchOptions = { - cwd: string; - args: string[]; - testProvider: TestProvider; - token?: CancellationToken; - outChannel?: OutputChannel; -}; - export const ITestDebugLauncher = Symbol('ITestDebugLauncher'); - export interface ITestDebugLauncher { launchDebugger(options: LaunchOptions): Promise; } export const ITestManagerFactory = Symbol('ITestManagerFactory'); - export interface ITestManagerFactory extends Function { // tslint:disable-next-line:callable-types (testProvider: TestProvider, workspaceFolder: Uri, rootDirectory: string): ITestManager; } -export const ITestManagerServiceFactory = Symbol('TestManagerServiceFactory'); +export const ITestManagerServiceFactory = Symbol('TestManagerServiceFactory'); export interface ITestManagerServiceFactory extends Function { // tslint:disable-next-line:callable-types (workspaceFolder: Uri): ITestManagerService; @@ -249,7 +251,6 @@ export interface ITestManager extends Disposable { } export const ITestDiscoveryService = Symbol('ITestDiscoveryService'); - export interface ITestDiscoveryService { discoverTests(options: TestDiscoveryOptions): Promise; } @@ -259,8 +260,6 @@ export interface ITestsParser { parse(content: string, options: ParserOptions): Tests; } -export type ParserOptions = TestDiscoveryOptions; - export const IUnitTestSocketServer = Symbol('IUnitTestSocketServer'); export interface IUnitTestSocketServer extends Disposable { on(event: string | symbol, listener: Function): this; @@ -270,34 +269,17 @@ export interface IUnitTestSocketServer extends Disposable { stop(): void; } -export type Options = { - workspaceFolder: Uri; - cwd: string; - args: string[]; - outChannel?: OutputChannel; - token: CancellationToken; -}; - export const ITestRunner = Symbol('ITestRunner'); export interface ITestRunner { run(testProvider: TestProvider, options: Options): Promise; } -export enum PassCalculationFormulae { - pytest, - nosetests -} - export const IXUnitParser = Symbol('IXUnitParser'); export interface IXUnitParser { - updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, passCalculationFormulae: PassCalculationFormulae): Promise; + // Update "tests" with the results parsed from the given file. + updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string): Promise; } -export type PythonVersionInformation = { - major: number; - minor: number; -}; - export const ITestMessageService = Symbol('ITestMessageService'); export interface ITestMessageService { getFilteredTestMessages(rootDirectory: string, testResults: Tests): Promise; @@ -321,6 +303,7 @@ export interface ITestDebugConfig extends DebugConfiguration { justMyCode?: boolean; subProcess?: boolean; } + export const ITestContextService = Symbol('ITestContextService'); export interface ITestContextService extends Disposable { register(): void; diff --git a/src/client/testing/common/xUnitParser.ts b/src/client/testing/common/xUnitParser.ts index e315ae9b8341..9467fb6674c8 100644 --- a/src/client/testing/common/xUnitParser.ts +++ b/src/client/testing/common/xUnitParser.ts @@ -1,6 +1,10 @@ -import * as fs from 'fs'; -import { injectable } from 'inversify'; -import { IXUnitParser, PassCalculationFormulae, Tests, TestStatus } from './types'; +import { inject, injectable } from 'inversify'; +import { IFileSystem } from '../../common/platform/types'; +import { + FlattenedTestFunction, IXUnitParser, + TestFunction, TestResult, Tests, TestStatus, TestSummary +} from './types'; + type TestSuiteResult = { $: { errors: string; @@ -44,104 +48,137 @@ function getSafeInt(value: string, defaultValue: any = 0): number { @injectable() export class XUnitParser implements IXUnitParser { - public updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, passCalculationFormulae: PassCalculationFormulae): Promise { - return updateResultsFromXmlLogFile(tests, outputXmlFile, passCalculationFormulae); + constructor( + @inject(IFileSystem) private readonly fs: IFileSystem + ) { } + + // Update "tests" with the results parsed from the given file. + public async updateResultsFromXmlLogFile( + tests: Tests, + outputXmlFile: string + ) { + const data = await this.fs.readFile(outputXmlFile); + + const parserResult = await parseXML(data) as { testsuite: TestSuiteResult }; + updateTests(tests, parserResult.testsuite); } } -export function updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, passCalculationFormulae: PassCalculationFormulae): Promise { + +// An async wrapper around xml2js.parseString(). +// tslint:disable-next-line:no-any +async function parseXML(data: string): Promise { + const xml2js = await import('xml2js'); // tslint:disable-next-line:no-any return new Promise((resolve, reject) => { - fs.readFile(outputXmlFile, 'utf8', (err, data) => { - if (err) { - return reject(err); + // tslint:disable-next-line:no-any + xml2js.parseString(data, (error: Error, result: any) => { + if (error) { + return reject(error); } - // tslint:disable-next-line:no-require-imports - const xml2js = require('xml2js'); - xml2js.parseString(data, (error: Error, parserResult: { testsuite: TestSuiteResult }) => { - if (error) { - return reject(error); - } - try { - const testSuiteResult: TestSuiteResult = parserResult.testsuite; - tests.summary.errors = getSafeInt(testSuiteResult.$.errors); - tests.summary.failures = getSafeInt(testSuiteResult.$.failures); - tests.summary.skipped = getSafeInt(testSuiteResult.$.skips ? testSuiteResult.$.skips : testSuiteResult.$.skip); - const testCount = getSafeInt(testSuiteResult.$.tests); - - switch (passCalculationFormulae) { - case PassCalculationFormulae.pytest: { - tests.summary.passed = testCount - tests.summary.failures - tests.summary.skipped - tests.summary.errors; - break; - } - case PassCalculationFormulae.nosetests: { - tests.summary.passed = testCount - tests.summary.failures - tests.summary.skipped - tests.summary.errors; - break; - } - default: { - throw new Error('Unknown Test Pass Calculation'); - } - } + return resolve(result); + }); + }); +} - if (!Array.isArray(testSuiteResult.testcase)) { - return resolve(); - } +// Update "tests" with the given results. +function updateTests( + tests: Tests, + testSuiteResult: TestSuiteResult +) { + updateSummary(tests.summary, testSuiteResult); - testSuiteResult.testcase.forEach((testcase: TestCaseResult) => { - const xmlClassName = testcase.$.classname.replace(/\(\)/g, '').replace(/\.\./g, '.').replace(/\.\./g, '.').replace(/\.+$/, ''); - const result = tests.testFunctions.find(fn => fn.xmlClassName === xmlClassName && fn.testFunction.name === testcase.$.name); - if (!result) { - // Possible we're dealing with nosetests, where the file name isn't returned to us - // When dealing with nose tests - // It is possible to have a test file named x in two separate test sub directories and have same functions/classes - // And unforutnately xunit log doesn't ouput the filename + if (!Array.isArray(testSuiteResult.testcase)) { + return; + } - // result = tests.testFunctions.find(fn => fn.testFunction.name === testcase.$.name && - // fn.parentTestSuite && fn.parentTestSuite.name === testcase.$.classname); + // Update the results for each test. + // Previously unknown tests are ignored. + testSuiteResult.testcase.forEach((testcase: TestCaseResult) => { + const testFunc = findTestFunction( + tests.testFunctions, + testcase.$.classname, + testcase.$.name + ); + if (testFunc) { + updateResultInfo(testFunc, testcase); + updateResultStatus(testFunc, testcase); + } else { + // Possible we're dealing with nosetests, where the file name isn't returned to us + // When dealing with nose tests + // It is possible to have a test file named x in two separate test sub directories and have same functions/classes + // And unforutnately xunit log doesn't ouput the filename - // Look for failed file test - const fileTest = testcase.$.file && tests.testFiles.find(file => file.nameToRun === testcase.$.file); - if (fileTest && testcase.error) { - fileTest.status = TestStatus.Error; - fileTest.passed = false; - fileTest.message = testcase.error[0].$.message; - fileTest.traceback = testcase.error[0]._; - } - return; - } + // result = tests.testFunctions.find(fn => fn.testFunction.name === testcase.$.name && + // fn.parentTestSuite && fn.parentTestSuite.name === testcase.$.classname); - result.testFunction.line = getSafeInt(testcase.$.line, null); - result.testFunction.file = testcase.$.file; - result.testFunction.time = parseFloat(testcase.$.time); - result.testFunction.passed = true; - result.testFunction.status = TestStatus.Pass; + // Look for failed file test + const fileTest = testcase.$.file && tests.testFiles.find(file => file.nameToRun === testcase.$.file); + if (fileTest && testcase.error) { + updateResultStatus(fileTest, testcase); + } + } + }); +} - if (testcase.failure) { - result.testFunction.status = TestStatus.Fail; - result.testFunction.passed = false; - result.testFunction.message = testcase.failure[0].$.message; - result.testFunction.traceback = testcase.failure[0]._; - } +// Update the summary with the information in the given results. +function updateSummary( + summary: TestSummary, + testSuiteResult: TestSuiteResult +) { + summary.errors = getSafeInt(testSuiteResult.$.errors); + summary.failures = getSafeInt(testSuiteResult.$.failures); + summary.skipped = getSafeInt(testSuiteResult.$.skips ? testSuiteResult.$.skips : testSuiteResult.$.skip); + const testCount = getSafeInt(testSuiteResult.$.tests); + summary.passed = testCount - summary.failures - summary.skipped - summary.errors; +} - if (testcase.error) { - result.testFunction.status = TestStatus.Error; - result.testFunction.passed = false; - result.testFunction.message = testcase.error[0].$.message; - result.testFunction.traceback = testcase.error[0]._; - } +function findTestFunction( + candidates: FlattenedTestFunction[], + className: string, + funcName: string +): TestFunction | undefined { + const xmlClassName = className + .replace(/\(\)/g, '') + .replace(/\.\./g, '.') + .replace(/\.\./g, '.') + .replace(/\.+$/, ''); + const flattened = candidates.find(fn => fn.xmlClassName === xmlClassName && fn.testFunction.name === funcName); + if (!flattened) { + return; + } + return flattened.testFunction; +} - if (testcase.skipped) { - result.testFunction.status = TestStatus.Skipped; - result.testFunction.passed = undefined; - result.testFunction.message = testcase.skipped[0].$.message; - result.testFunction.traceback = ''; - } - }); - } catch (ex) { - return reject(ex); - } +function updateResultInfo( + result: TestResult, + testCase: TestCaseResult +) { + result.file = testCase.$.file; + result.line = getSafeInt(testCase.$.line, null); + result.time = parseFloat(testCase.$.time); +} - resolve(); - }); - }); - }); +function updateResultStatus( + result: TestResult, + testCase: TestCaseResult +) { + if (testCase.error) { + result.status = TestStatus.Error; + result.passed = false; + result.message = testCase.error[0].$.message; + result.traceback = testCase.error[0]._; + } else if (testCase.failure) { + result.status = TestStatus.Fail; + result.passed = false; + result.message = testCase.failure[0].$.message; + result.traceback = testCase.failure[0]._; + } else if (testCase.skipped) { + result.status = TestStatus.Skipped; + result.passed = undefined; + result.message = testCase.skipped[0].$.message; + result.traceback = ''; + } else { + result.status = TestStatus.Pass; + result.passed = true; + } } diff --git a/src/client/testing/explorer/commandHandlers.ts b/src/client/testing/explorer/commandHandlers.ts index 66b5ee58d225..1b7d6d0e90dd 100644 --- a/src/client/testing/explorer/commandHandlers.ts +++ b/src/client/testing/explorer/commandHandlers.ts @@ -10,19 +10,18 @@ import { traceDecorators } from '../../common/logger'; import { IDisposable } from '../../common/types'; import { swallowExceptions } from '../../common/utils/decorators'; import { CommandSource } from '../common/constants'; -import { getTestType } from '../common/testUtils'; +import { getTestDataItemType } from '../common/testUtils'; import { - TestFile, TestFolder, TestFunction, - TestsToRun, TestSuite, TestType + TestFile, TestFolder, TestFunction, TestsToRun, TestSuite } from '../common/types'; import { ITestExplorerCommandHandler } from '../navigation/types'; -import { ITestDataItemResource, TestDataItem } from '../types'; +import { ITestDataItemResource, TestDataItem, TestDataItemType } from '../types'; type NavigationCommands = typeof Commands.navigateToTestFile | typeof Commands.navigateToTestFunction | typeof Commands.navigateToTestSuite; const testNavigationCommandMapping: { [key: string]: NavigationCommands } = { - [TestType.testFile]: Commands.navigateToTestFile, - [TestType.testFunction]: Commands.navigateToTestFunction, - [TestType.testSuite]: Commands.navigateToTestSuite + [TestDataItemType.file]: Commands.navigateToTestFile, + [TestDataItemType.function]: Commands.navigateToTestFunction, + [TestDataItemType.suite]: Commands.navigateToTestSuite }; @injectable() @@ -53,8 +52,8 @@ export class TestExplorerCommandHandler implements ITestExplorerCommandHandler { @swallowExceptions('Open test node in Editor') @traceDecorators.error('Open test node in editor failed') protected async onOpenTestNodeInEditor(item: TestDataItem): Promise { - const testType = getTestType(item); - if (testType === TestType.testFolder) { + const testType = getTestDataItemType(item); + if (testType === TestDataItemType.folder) { throw new Error('Unknown Test Type'); } const command = testNavigationCommandMapping[testType]; @@ -68,20 +67,20 @@ export class TestExplorerCommandHandler implements ITestExplorerCommandHandler { protected async runDebugTestNode(item: TestDataItem, runType: 'run' | 'debug'): Promise { let testToRun: TestsToRun; - switch (getTestType(item)) { - case TestType.testFile: { + switch (getTestDataItemType(item)) { + case TestDataItemType.file: { testToRun = { testFile: [item as TestFile] }; break; } - case TestType.testFolder: { + case TestDataItemType.folder: { testToRun = { testFolder: [item as TestFolder] }; break; } - case TestType.testSuite: { + case TestDataItemType.suite: { testToRun = { testSuite: [item as TestSuite] }; break; } - case TestType.testFunction: { + case TestDataItemType.function: { testToRun = { testFunction: [item as TestFunction] }; break; } diff --git a/src/client/testing/explorer/failedTestHandler.ts b/src/client/testing/explorer/failedTestHandler.ts index 090ef95945db..21973e564ed5 100644 --- a/src/client/testing/explorer/failedTestHandler.ts +++ b/src/client/testing/explorer/failedTestHandler.ts @@ -11,9 +11,9 @@ import { Commands } from '../../common/constants'; import '../../common/extensions'; import { IDisposable, IDisposableRegistry } from '../../common/types'; import { debounceAsync } from '../../common/utils/decorators'; -import { getTestType } from '../common/testUtils'; -import { ITestCollectionStorageService, TestStatus, TestType } from '../common/types'; -import { TestDataItem } from '../types'; +import { getTestDataItemType } from '../common/testUtils'; +import { ITestCollectionStorageService, TestStatus } from '../common/types'; +import { TestDataItem, TestDataItemType } from '../types'; @injectable() export class FailedTestHandler implements IExtensionSingleActivationService, IDisposable { @@ -32,7 +32,7 @@ export class FailedTestHandler implements IExtensionSingleActivationService, IDi } public onDidChangeTestData(args: { uri: Uri; data?: TestDataItem }): void { if (args.data && (args.data.status === TestStatus.Error || args.data.status === TestStatus.Fail) && - getTestType(args.data) === TestType.testFunction) { + getTestDataItemType(args.data) === TestDataItemType.function) { this.failedItems.push(args.data); this.revealFailedNodes().ignoreErrors(); } diff --git a/src/client/testing/explorer/testTreeViewItem.ts b/src/client/testing/explorer/testTreeViewItem.ts index dd4d614a47e5..0b34e3eeb07b 100644 --- a/src/client/testing/explorer/testTreeViewItem.ts +++ b/src/client/testing/explorer/testTreeViewItem.ts @@ -10,12 +10,12 @@ import { Commands } from '../../common/constants'; import { getIcon } from '../../common/utils/icons'; import { noop } from '../../common/utils/misc'; import { Icons } from '../common/constants'; -import { getTestType, isSubtestsParent } from '../common/testUtils'; -import { TestResult, TestStatus, TestSuite, TestType } from '../common/types'; -import { TestDataItem } from '../types'; +import { getTestDataItemType, isSubtestsParent } from '../common/testUtils'; +import { TestResult, TestStatus, TestSuite } from '../common/types'; +import { TestDataItem, TestDataItemType } from '../types'; function getDefaultCollapsibleState(data: TestDataItem): TreeItemCollapsibleState { - return getTestType(data) === TestType.testFunction ? TreeItemCollapsibleState.None : TreeItemCollapsibleState.Collapsed; + return getTestDataItemType(data) === TestDataItemType.function ? TreeItemCollapsibleState.None : TreeItemCollapsibleState.Collapsed; } /** @@ -24,7 +24,7 @@ function getDefaultCollapsibleState(data: TestDataItem): TreeItemCollapsibleStat * TestDataItem. */ export class TestTreeItem extends TreeItem { - public readonly testType: TestType; + public readonly testType: TestDataItemType; constructor( public readonly resource: Uri, @@ -32,7 +32,7 @@ export class TestTreeItem extends TreeItem { collapsibleStatue: TreeItemCollapsibleState = getDefaultCollapsibleState(data) ) { super(data.name, collapsibleStatue); - this.testType = getTestType(this.data); + this.testType = getTestDataItemType(this.data); this.setCommand(); } public get contextValue(): string { @@ -40,7 +40,7 @@ export class TestTreeItem extends TreeItem { } public get iconPath(): string | Uri | { light: string | Uri; dark: string | Uri } | ThemeIcon { - if (this.testType === TestType.testWorkspaceFolder) { + if (this.testType === TestDataItemType.workspaceFolder) { return ThemeIcon.Folder; } if (!this.data) { @@ -70,14 +70,14 @@ export class TestTreeItem extends TreeItem { } public get tooltip(): string { - if (!this.data || this.testType === TestType.testWorkspaceFolder) { + if (!this.data || this.testType === TestDataItemType.workspaceFolder) { return ''; } const result = this.data as TestResult; if (!result.status || result.status === TestStatus.Idle || result.status === TestStatus.Unknown || result.status === TestStatus.Skipped) { return ''; } - if (this.testType !== TestType.testFunction) { + if (this.testType !== TestDataItemType.function) { if (result.functionsPassed === undefined) { return ''; } @@ -113,15 +113,15 @@ export class TestTreeItem extends TreeItem { private setCommand() { switch (this.testType) { - case TestType.testFile: { + case TestDataItemType.file: { this.command = { command: Commands.navigateToTestFile, title: 'Open', arguments: [this.resource, this.data] }; break; } - case TestType.testFunction: { + case TestDataItemType.function: { this.command = { command: Commands.navigateToTestFunction, title: 'Open', arguments: [this.resource, this.data, false] }; break; } - case TestType.testSuite: { + case TestDataItemType.suite: { if (isSubtestsParent(this.data as TestSuite)) { this.command = { command: Commands.navigateToTestFunction, title: 'Open', arguments: [this.resource, this.data, false] }; break; diff --git a/src/client/testing/explorer/testTreeViewProvider.ts b/src/client/testing/explorer/testTreeViewProvider.ts index d9735086c6ec..54aaf7abd9a4 100644 --- a/src/client/testing/explorer/testTreeViewProvider.ts +++ b/src/client/testing/explorer/testTreeViewProvider.ts @@ -11,9 +11,12 @@ import { IDisposable, IDisposableRegistry } from '../../common/types'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { CommandSource } from '../common/constants'; -import { getChildren, getParent, getTestType } from '../common/testUtils'; -import { ITestCollectionStorageService, Tests, TestStatus, TestType } from '../common/types'; -import { ITestDataItemResource, ITestManagementService, ITestTreeViewProvider, TestDataItem, TestWorkspaceFolder, WorkspaceTestStatus } from '../types'; +import { getChildren, getParent, getTestDataItemType } from '../common/testUtils'; +import { ITestCollectionStorageService, Tests, TestStatus } from '../common/types'; +import { + ITestDataItemResource, ITestManagementService, ITestTreeViewProvider, + TestDataItem, TestDataItemType, TestWorkspaceFolder, WorkspaceTestStatus +} from '../types'; import { TestTreeItem } from './testTreeViewItem'; @injectable() @@ -182,7 +185,7 @@ export class TestTreeViewProvider implements ITestTreeViewProvider, ITestDataIte private async shouldElementBeExpandedByDefault(element: TestDataItem) { const parent = await this.getParent(element); - if (!parent || getTestType(parent) === TestType.testWorkspaceFolder) { + if (!parent || getTestDataItemType(parent) === TestDataItemType.workspaceFolder) { return true; } return false; diff --git a/src/client/testing/nosetest/runner.ts b/src/client/testing/nosetest/runner.ts index 692e038aca8b..0f2dfa5fc845 100644 --- a/src/client/testing/nosetest/runner.ts +++ b/src/client/testing/nosetest/runner.ts @@ -6,7 +6,10 @@ import { noop } from '../../common/utils/misc'; import { IServiceContainer } from '../../ioc/types'; import { NOSETEST_PROVIDER } from '../common/constants'; import { Options } from '../common/runner'; -import { ITestDebugLauncher, ITestManager, ITestResultsService, ITestRunner, IXUnitParser, LaunchOptions, PassCalculationFormulae, TestRunOptions, Tests } from '../common/types'; +import { + ITestDebugLauncher, ITestManager, ITestResultsService, ITestRunner, + IXUnitParser, LaunchOptions, TestRunOptions, Tests +} from '../common/types'; import { IArgumentsHelper, IArgumentsService, ITestManagerRunner } from '../types'; const WITH_XUNIT = '--with-xunit'; @@ -84,7 +87,7 @@ export class TestManagerRunner implements ITestManagerRunner { } private async updateResultsFromLogFiles(tests: Tests, outputXmlFile: string, testResultsService: ITestResultsService): Promise { - await this.xUnitParser.updateResultsFromXmlLogFile(tests, outputXmlFile, PassCalculationFormulae.nosetests); + await this.xUnitParser.updateResultsFromXmlLogFile(tests, outputXmlFile); testResultsService.updateResults(tests); return tests; } diff --git a/src/client/testing/pytest/runner.ts b/src/client/testing/pytest/runner.ts index f10784011273..09014b150215 100644 --- a/src/client/testing/pytest/runner.ts +++ b/src/client/testing/pytest/runner.ts @@ -5,7 +5,10 @@ import { noop } from '../../common/utils/misc'; import { IServiceContainer } from '../../ioc/types'; import { PYTEST_PROVIDER } from '../common/constants'; import { Options } from '../common/runner'; -import { ITestDebugLauncher, ITestManager, ITestResultsService, ITestRunner, IXUnitParser, LaunchOptions, PassCalculationFormulae, TestRunOptions, Tests } from '../common/types'; +import { + ITestDebugLauncher, ITestManager, ITestResultsService, ITestRunner, + IXUnitParser, LaunchOptions, TestRunOptions, Tests +} from '../common/types'; import { IArgumentsHelper, IArgumentsService, ITestManagerRunner } from '../types'; const JunitXmlArg = '--junitxml'; @@ -76,7 +79,7 @@ export class TestManagerRunner implements ITestManagerRunner { } private async updateResultsFromLogFiles(tests: Tests, outputXmlFile: string, testResultsService: ITestResultsService): Promise { - await this.xUnitParser.updateResultsFromXmlLogFile(tests, outputXmlFile, PassCalculationFormulae.pytest); + await this.xUnitParser.updateResultsFromXmlLogFile(tests, outputXmlFile); testResultsService.updateResults(tests); return tests; } diff --git a/src/client/testing/types.ts b/src/client/testing/types.ts index 46e2e62de579..082ab94a7a8e 100644 --- a/src/client/testing/types.ts +++ b/src/client/testing/types.ts @@ -157,6 +157,13 @@ export interface ILocationStackFrameDetails { export type WorkspaceTestStatus = { workspace: Uri; status: TestStatus }; +export enum TestDataItemType { + workspaceFolder = 'workspaceFolder', + folder = 'folder', + file = 'file', + suite = 'suite', + function = 'function' +} export type TestDataItem = TestWorkspaceFolder | TestFolder | TestFile | TestSuite | TestFunction; export class TestWorkspaceFolder { diff --git a/src/test/testing/common/services/storageService.unit.test.ts b/src/test/testing/common/services/storageService.unit.test.ts index 46869040520a..fa112024d810 100644 --- a/src/test/testing/common/services/storageService.unit.test.ts +++ b/src/test/testing/common/services/storageService.unit.test.ts @@ -5,7 +5,11 @@ import * as assert from 'assert'; import { copyDesiredTestResults } from '../../../../client/testing/common/testUtils'; -import { FlattenedTestFunction, FlattenedTestSuite, TestFile, TestFolder, TestFunction, Tests, TestStatus, TestSuite, TestType } from '../../../../client/testing/common/types'; +import { + FlattenedTestFunction, FlattenedTestSuite, TestFile, TestFolder, + TestFunction, Tests, TestStatus, TestSuite +} from '../../../../client/testing/common/types'; +import { TestDataItemType } from '../../../../client/testing/types'; import { createMockTestDataItem } from '../testUtils.unit.test'; // tslint:disable:no-any max-func-body-length @@ -18,14 +22,14 @@ suite('Unit Tests - Storage Service', () => { }); function setupTestData1() { - const folder1 = createMockTestDataItem(TestType.testFolder, '1'); - const file1 = createMockTestDataItem(TestType.testFile, '1'); + const folder1 = createMockTestDataItem(TestDataItemType.folder, '1'); + const file1 = createMockTestDataItem(TestDataItemType.file, '1'); folder1.testFiles.push(file1); - const suite1 = createMockTestDataItem(TestType.testSuite, '1'); - const suite2 = createMockTestDataItem(TestType.testSuite, '2'); - const fn1 = createMockTestDataItem(TestType.testFunction, '1'); - const fn2 = createMockTestDataItem(TestType.testFunction, '2'); - const fn3 = createMockTestDataItem(TestType.testFunction, '3'); + const suite1 = createMockTestDataItem(TestDataItemType.suite, '1'); + const suite2 = createMockTestDataItem(TestDataItemType.suite, '2'); + const fn1 = createMockTestDataItem(TestDataItemType.function, '1'); + const fn2 = createMockTestDataItem(TestDataItemType.function, '2'); + const fn3 = createMockTestDataItem(TestDataItemType.function, '3'); file1.suites.push(suite1); file1.suites.push(suite2); file1.functions.push(fn1); @@ -62,14 +66,14 @@ suite('Unit Tests - Storage Service', () => { } function setupTestData2() { - const folder1 = createMockTestDataItem(TestType.testFolder, '1'); - const file1 = createMockTestDataItem(TestType.testFile, '1'); + const folder1 = createMockTestDataItem(TestDataItemType.folder, '1'); + const file1 = createMockTestDataItem(TestDataItemType.file, '1'); folder1.testFiles.push(file1); - const suite1 = createMockTestDataItem(TestType.testSuite, '1'); - const suite2 = createMockTestDataItem(TestType.testSuite, '2'); - const fn1 = createMockTestDataItem(TestType.testFunction, '1'); - const fn2 = createMockTestDataItem(TestType.testFunction, '2'); - const fn3 = createMockTestDataItem(TestType.testFunction, '3'); + const suite1 = createMockTestDataItem(TestDataItemType.suite, '1'); + const suite2 = createMockTestDataItem(TestDataItemType.suite, '2'); + const fn1 = createMockTestDataItem(TestDataItemType.function, '1'); + const fn2 = createMockTestDataItem(TestDataItemType.function, '2'); + const fn3 = createMockTestDataItem(TestDataItemType.function, '3'); file1.suites.push(suite1); file1.suites.push(suite2); suite1.functions.push(fn1); diff --git a/src/test/testing/common/services/testResultsService.unit.test.ts b/src/test/testing/common/services/testResultsService.unit.test.ts index 48a73ff09e7e..b12673daff7a 100644 --- a/src/test/testing/common/services/testResultsService.unit.test.ts +++ b/src/test/testing/common/services/testResultsService.unit.test.ts @@ -6,7 +6,11 @@ import { expect } from 'chai'; import * as typemoq from 'typemoq'; import { TestResultsService } from '../../../../client/testing/common/services/testResultsService'; -import { FlattenedTestFunction, FlattenedTestSuite, ITestVisitor, TestFile, TestFolder, TestFunction, Tests, TestStatus, TestSuite, TestType } from '../../../../client/testing/common/types'; +import { + FlattenedTestFunction, FlattenedTestSuite, ITestVisitor, TestFile, + TestFolder, TestFunction, Tests, TestStatus, TestSuite +} from '../../../../client/testing/common/types'; +import { TestDataItemType } from '../../../../client/testing/types'; import { createMockTestDataItem } from '../testUtils.unit.test'; // tslint:disable:no-any max-func-body-length @@ -19,53 +23,53 @@ suite('Unit Tests - Tests Results Service', () => { let file1: TestFile, file2: TestFile, file3: TestFile, file4: TestFile, file5: TestFile; setup(() => { resultResetVisitor = typemoq.Mock.ofType(); - folder1 = createMockTestDataItem(TestType.testFolder); - folder2 = createMockTestDataItem(TestType.testFolder); - folder3 = createMockTestDataItem(TestType.testFolder); - folder4 = createMockTestDataItem(TestType.testFolder); - folder5 = createMockTestDataItem(TestType.testFolder); + folder1 = createMockTestDataItem(TestDataItemType.folder); + folder2 = createMockTestDataItem(TestDataItemType.folder); + folder3 = createMockTestDataItem(TestDataItemType.folder); + folder4 = createMockTestDataItem(TestDataItemType.folder); + folder5 = createMockTestDataItem(TestDataItemType.folder); folder1.folders.push(folder2); folder1.folders.push(folder3); folder2.folders.push(folder4); folder3.folders.push(folder5); - file1 = createMockTestDataItem(TestType.testFile); - file2 = createMockTestDataItem(TestType.testFile); - file3 = createMockTestDataItem(TestType.testFile); - file4 = createMockTestDataItem(TestType.testFile); - file5 = createMockTestDataItem(TestType.testFile); + file1 = createMockTestDataItem(TestDataItemType.file); + file2 = createMockTestDataItem(TestDataItemType.file); + file3 = createMockTestDataItem(TestDataItemType.file); + file4 = createMockTestDataItem(TestDataItemType.file); + file5 = createMockTestDataItem(TestDataItemType.file); folder1.testFiles.push(file1); folder3.testFiles.push(file2); folder3.testFiles.push(file3); folder4.testFiles.push(file5); folder5.testFiles.push(file4); - suite1 = createMockTestDataItem(TestType.testSuite); - suite2 = createMockTestDataItem(TestType.testSuite); - suite3 = createMockTestDataItem(TestType.testSuite); - suite4 = createMockTestDataItem(TestType.testSuite); - suite5 = createMockTestDataItem(TestType.testSuite); - const fn1 = createMockTestDataItem(TestType.testFunction); + suite1 = createMockTestDataItem(TestDataItemType.suite); + suite2 = createMockTestDataItem(TestDataItemType.suite); + suite3 = createMockTestDataItem(TestDataItemType.suite); + suite4 = createMockTestDataItem(TestDataItemType.suite); + suite5 = createMockTestDataItem(TestDataItemType.suite); + const fn1 = createMockTestDataItem(TestDataItemType.function); fn1.passed = true; - const fn2 = createMockTestDataItem(TestType.testFunction); + const fn2 = createMockTestDataItem(TestDataItemType.function); fn2.passed = undefined; - const fn3 = createMockTestDataItem(TestType.testFunction); + const fn3 = createMockTestDataItem(TestDataItemType.function); fn3.passed = true; - const fn4 = createMockTestDataItem(TestType.testFunction); + const fn4 = createMockTestDataItem(TestDataItemType.function); fn4.passed = false; - const fn5 = createMockTestDataItem(TestType.testFunction); + const fn5 = createMockTestDataItem(TestDataItemType.function); fn5.passed = undefined; - const fn6 = createMockTestDataItem(TestType.testFunction); + const fn6 = createMockTestDataItem(TestDataItemType.function); fn6.passed = true; - const fn7 = createMockTestDataItem(TestType.testFunction); + const fn7 = createMockTestDataItem(TestDataItemType.function); fn7.passed = undefined; - const fn8 = createMockTestDataItem(TestType.testFunction); + const fn8 = createMockTestDataItem(TestDataItemType.function); fn8.passed = false; - const fn9 = createMockTestDataItem(TestType.testFunction); + const fn9 = createMockTestDataItem(TestDataItemType.function); fn9.passed = true; - const fn10 = createMockTestDataItem(TestType.testFunction); + const fn10 = createMockTestDataItem(TestDataItemType.function); fn10.passed = true; - const fn11 = createMockTestDataItem(TestType.testFunction); + const fn11 = createMockTestDataItem(TestDataItemType.function); fn11.passed = true; file1.suites.push(suite1); file1.suites.push(suite2); diff --git a/src/test/testing/common/services/testStatusService.unit.test.ts b/src/test/testing/common/services/testStatusService.unit.test.ts index 43f5e9880d7e..83d88a7a0a47 100644 --- a/src/test/testing/common/services/testStatusService.unit.test.ts +++ b/src/test/testing/common/services/testStatusService.unit.test.ts @@ -9,8 +9,12 @@ import { Uri } from 'vscode'; import { TestCollectionStorageService } from '../../../../client/testing/common/services/storageService'; import { TestsStatusUpdaterService } from '../../../../client/testing/common/services/testsStatusService'; import { visitRecursive } from '../../../../client/testing/common/testVisitors/visitor'; -import { FlattenedTestFunction, FlattenedTestSuite, ITestCollectionStorageService, ITestsStatusUpdaterService, TestFile, TestFolder, TestFunction, Tests, TestStatus, TestSuite, TestType } from '../../../../client/testing/common/types'; -import { TestDataItem } from '../../../../client/testing/types'; +import { + FlattenedTestFunction, FlattenedTestSuite, ITestCollectionStorageService, + ITestsStatusUpdaterService, TestFile, TestFolder, TestFunction, Tests, + TestStatus, TestSuite +} from '../../../../client/testing/common/types'; +import { TestDataItem, TestDataItemType } from '../../../../client/testing/types'; import { createMockTestDataItem } from '../testUtils.unit.test'; // tslint:disable:no-any max-func-body-length @@ -22,35 +26,35 @@ suite('Unit Tests - Tests Status Updater', () => { setup(() => { storage = mock(TestCollectionStorageService); updater = new TestsStatusUpdaterService(instance(storage)); - const folder1 = createMockTestDataItem(TestType.testFolder); - const folder2 = createMockTestDataItem(TestType.testFolder); - const folder3 = createMockTestDataItem(TestType.testFolder); - const folder4 = createMockTestDataItem(TestType.testFolder); - const folder5 = createMockTestDataItem(TestType.testFolder); + const folder1 = createMockTestDataItem(TestDataItemType.folder); + const folder2 = createMockTestDataItem(TestDataItemType.folder); + const folder3 = createMockTestDataItem(TestDataItemType.folder); + const folder4 = createMockTestDataItem(TestDataItemType.folder); + const folder5 = createMockTestDataItem(TestDataItemType.folder); folder1.folders.push(folder2); folder1.folders.push(folder3); folder2.folders.push(folder4); folder3.folders.push(folder5); - const file1 = createMockTestDataItem(TestType.testFile); - const file2 = createMockTestDataItem(TestType.testFile); - const file3 = createMockTestDataItem(TestType.testFile); - const file4 = createMockTestDataItem(TestType.testFile); + const file1 = createMockTestDataItem(TestDataItemType.file); + const file2 = createMockTestDataItem(TestDataItemType.file); + const file3 = createMockTestDataItem(TestDataItemType.file); + const file4 = createMockTestDataItem(TestDataItemType.file); folder1.testFiles.push(file1); folder3.testFiles.push(file2); folder3.testFiles.push(file3); folder5.testFiles.push(file4); - const suite1 = createMockTestDataItem(TestType.testSuite); - const suite2 = createMockTestDataItem(TestType.testSuite); - const suite3 = createMockTestDataItem(TestType.testSuite); - const suite4 = createMockTestDataItem(TestType.testSuite); - const suite5 = createMockTestDataItem(TestType.testSuite); - const fn1 = createMockTestDataItem(TestType.testFunction); - const fn2 = createMockTestDataItem(TestType.testFunction); - const fn3 = createMockTestDataItem(TestType.testFunction); - const fn4 = createMockTestDataItem(TestType.testFunction); - const fn5 = createMockTestDataItem(TestType.testFunction); + const suite1 = createMockTestDataItem(TestDataItemType.suite); + const suite2 = createMockTestDataItem(TestDataItemType.suite); + const suite3 = createMockTestDataItem(TestDataItemType.suite); + const suite4 = createMockTestDataItem(TestDataItemType.suite); + const suite5 = createMockTestDataItem(TestDataItemType.suite); + const fn1 = createMockTestDataItem(TestDataItemType.function); + const fn2 = createMockTestDataItem(TestDataItemType.function); + const fn3 = createMockTestDataItem(TestDataItemType.function); + const fn4 = createMockTestDataItem(TestDataItemType.function); + const fn5 = createMockTestDataItem(TestDataItemType.function); file1.suites.push(suite1); file1.suites.push(suite2); file3.suites.push(suite3); diff --git a/src/test/testing/common/testUtils.unit.test.ts b/src/test/testing/common/testUtils.unit.test.ts index 2d907c796419..4e51af938051 100644 --- a/src/test/testing/common/testUtils.unit.test.ts +++ b/src/test/testing/common/testUtils.unit.test.ts @@ -8,15 +8,15 @@ import * as path from 'path'; import { Uri } from 'vscode'; import { getNamesAndValues } from '../../../client/common/utils/enum'; import { - getChildren, getParent, getParentFile, getParentSuite, getTestFile, - getTestFolder, getTestFunction, getTestSuite, getTestType + getChildren, getParent, getParentFile, getParentSuite, getTestDataItemType, + getTestFile, getTestFolder, getTestFunction, getTestSuite } from '../../../client/testing/common/testUtils'; import { FlattenedTestFunction, FlattenedTestSuite, SubtestParent, TestFile, - TestFolder, TestFunction, Tests, TestSuite, TestType + TestFolder, TestFunction, Tests, TestSuite } from '../../../client/testing/common/types'; import { - TestDataItem, TestWorkspaceFolder + TestDataItem, TestDataItemType, TestWorkspaceFolder } from '../../../client/testing/types'; // tslint:disable:prefer-template @@ -36,7 +36,7 @@ function longestCommonSubstring(strings: string[]): string { } export function createMockTestDataItem( - type: TestType, + type: TestDataItemType, nameSuffix: string = '', name?: string, nameToRun?: string @@ -78,18 +78,18 @@ export function createMockTestDataItem( }; switch (type) { - case TestType.testFile: + case TestDataItemType.file: return file as T; - case TestType.testFolder: + case TestDataItemType.folder: return folder as T; - case TestType.testFunction: + case TestDataItemType.function: return func as T; - case TestType.testSuite: + case TestDataItemType.suite: return suite as T; - case TestType.testWorkspaceFolder: + case TestDataItemType.workspaceFolder: return (new TestWorkspaceFolder({ uri: Uri.file(''), name: 'a', index: 0 })) as T; default: - throw new Error('Unknown type'); + throw new Error(`Unknown type ${type}`); } } @@ -147,24 +147,24 @@ export function createTests( // tslint:disable:max-func-body-length no-any suite('Unit Tests - TestUtils', () => { - test('Get TestType for Folders', () => { - const item = createMockTestDataItem(TestType.testFolder); - assert.equal(getTestType(item), TestType.testFolder); + test('Get TestDataItemType for Folders', () => { + const item = createMockTestDataItem(TestDataItemType.folder); + assert.equal(getTestDataItemType(item), TestDataItemType.folder); }); - test('Get TestType for Files', () => { - const item = createMockTestDataItem(TestType.testFile); - assert.equal(getTestType(item), TestType.testFile); + test('Get TestDataItemType for Files', () => { + const item = createMockTestDataItem(TestDataItemType.file); + assert.equal(getTestDataItemType(item), TestDataItemType.file); }); - test('Get TestType for Functions', () => { - const item = createMockTestDataItem(TestType.testFunction); - assert.equal(getTestType(item), TestType.testFunction); + test('Get TestDataItemType for Functions', () => { + const item = createMockTestDataItem(TestDataItemType.function); + assert.equal(getTestDataItemType(item), TestDataItemType.function); }); - test('Get TestType for Suites', () => { - const item = createMockTestDataItem(TestType.testSuite); - assert.equal(getTestType(item), TestType.testSuite); + test('Get TestDataItemType for Suites', () => { + const item = createMockTestDataItem(TestDataItemType.suite); + assert.equal(getTestDataItemType(item), TestDataItemType.suite); }); test('Casting to a specific items', () => { - for (const typeName of getNamesAndValues(TestType)) { + for (const typeName of getNamesAndValues(TestDataItemType)) { const item = createMockTestDataItem(typeName.value); const file = getTestFile(item); const folder = getTestFolder(item); @@ -172,7 +172,7 @@ suite('Unit Tests - TestUtils', () => { const func = getTestFunction(item); switch (typeName.value) { - case TestType.testFile: + case TestDataItemType.file: { assert.equal(file, item); assert.equal(folder, undefined); @@ -180,7 +180,7 @@ suite('Unit Tests - TestUtils', () => { assert.equal(func, undefined); break; } - case TestType.testFolder: + case TestDataItemType.folder: { assert.equal(file, undefined); assert.equal(folder, item); @@ -188,7 +188,7 @@ suite('Unit Tests - TestUtils', () => { assert.equal(func, undefined); break; } - case TestType.testFunction: + case TestDataItemType.function: { assert.equal(file, undefined); assert.equal(folder, undefined); @@ -196,7 +196,7 @@ suite('Unit Tests - TestUtils', () => { assert.equal(func, item); break; } - case TestType.testSuite: + case TestDataItemType.suite: { assert.equal(file, undefined); assert.equal(folder, undefined); @@ -204,7 +204,7 @@ suite('Unit Tests - TestUtils', () => { assert.equal(func, undefined); break; } - case TestType.testWorkspaceFolder: + case TestDataItemType.workspaceFolder: { assert.equal(file, undefined); assert.equal(folder, undefined); @@ -218,11 +218,11 @@ suite('Unit Tests - TestUtils', () => { } }); test('Get Parent of folder', () => { - const folder1 = createMockTestDataItem(TestType.testFolder); - const folder2 = createMockTestDataItem(TestType.testFolder); - const folder3 = createMockTestDataItem(TestType.testFolder); - const folder4 = createMockTestDataItem(TestType.testFolder); - const folder5 = createMockTestDataItem(TestType.testFolder); + const folder1 = createMockTestDataItem(TestDataItemType.folder); + const folder2 = createMockTestDataItem(TestDataItemType.folder); + const folder3 = createMockTestDataItem(TestDataItemType.folder); + const folder4 = createMockTestDataItem(TestDataItemType.folder); + const folder5 = createMockTestDataItem(TestDataItemType.folder); folder1.folders.push(folder2); folder1.folders.push(folder3); folder2.folders.push(folder4); @@ -242,20 +242,20 @@ suite('Unit Tests - TestUtils', () => { assert.equal(getParent(tests, folder5), folder3); }); test('Get Parent of file', () => { - const folder1 = createMockTestDataItem(TestType.testFolder); - const folder2 = createMockTestDataItem(TestType.testFolder); - const folder3 = createMockTestDataItem(TestType.testFolder); - const folder4 = createMockTestDataItem(TestType.testFolder); - const folder5 = createMockTestDataItem(TestType.testFolder); + const folder1 = createMockTestDataItem(TestDataItemType.folder); + const folder2 = createMockTestDataItem(TestDataItemType.folder); + const folder3 = createMockTestDataItem(TestDataItemType.folder); + const folder4 = createMockTestDataItem(TestDataItemType.folder); + const folder5 = createMockTestDataItem(TestDataItemType.folder); folder1.folders.push(folder2); folder1.folders.push(folder3); folder2.folders.push(folder4); folder3.folders.push(folder5); - const file1 = createMockTestDataItem(TestType.testFile); - const file2 = createMockTestDataItem(TestType.testFile); - const file3 = createMockTestDataItem(TestType.testFile); - const file4 = createMockTestDataItem(TestType.testFile); + const file1 = createMockTestDataItem(TestDataItemType.file); + const file2 = createMockTestDataItem(TestDataItemType.file); + const file3 = createMockTestDataItem(TestDataItemType.file); + const file4 = createMockTestDataItem(TestDataItemType.file); folder1.testFiles.push(file1); folder3.testFiles.push(file2); folder3.testFiles.push(file3); @@ -274,20 +274,20 @@ suite('Unit Tests - TestUtils', () => { assert.equal(getParent(tests, file4), folder5); }); test('Get Parent File', () => { - const file1 = createMockTestDataItem(TestType.testFile); - const file2 = createMockTestDataItem(TestType.testFile); - const file3 = createMockTestDataItem(TestType.testFile); - const file4 = createMockTestDataItem(TestType.testFile); - const suite1 = createMockTestDataItem(TestType.testSuite); - const suite2 = createMockTestDataItem(TestType.testSuite); - const suite3 = createMockTestDataItem(TestType.testSuite); - const suite4 = createMockTestDataItem(TestType.testSuite); - const suite5 = createMockTestDataItem(TestType.testSuite); - const fn1 = createMockTestDataItem(TestType.testFunction); - const fn2 = createMockTestDataItem(TestType.testFunction); - const fn3 = createMockTestDataItem(TestType.testFunction); - const fn4 = createMockTestDataItem(TestType.testFunction); - const fn5 = createMockTestDataItem(TestType.testFunction); + const file1 = createMockTestDataItem(TestDataItemType.file); + const file2 = createMockTestDataItem(TestDataItemType.file); + const file3 = createMockTestDataItem(TestDataItemType.file); + const file4 = createMockTestDataItem(TestDataItemType.file); + const suite1 = createMockTestDataItem(TestDataItemType.suite); + const suite2 = createMockTestDataItem(TestDataItemType.suite); + const suite3 = createMockTestDataItem(TestDataItemType.suite); + const suite4 = createMockTestDataItem(TestDataItemType.suite); + const suite5 = createMockTestDataItem(TestDataItemType.suite); + const fn1 = createMockTestDataItem(TestDataItemType.function); + const fn2 = createMockTestDataItem(TestDataItemType.function); + const fn3 = createMockTestDataItem(TestDataItemType.function); + const fn4 = createMockTestDataItem(TestDataItemType.function); + const fn5 = createMockTestDataItem(TestDataItemType.function); file1.suites.push(suite1); file1.suites.push(suite2); file3.suites.push(suite3); @@ -361,20 +361,20 @@ suite('Unit Tests - TestUtils', () => { assert.equal(getParentFile(tests, suite5), file3); }); test('Get Parent Suite', () => { - const file1 = createMockTestDataItem(TestType.testFile); - const file2 = createMockTestDataItem(TestType.testFile); - const file3 = createMockTestDataItem(TestType.testFile); - const file4 = createMockTestDataItem(TestType.testFile); - const suite1 = createMockTestDataItem(TestType.testSuite); - const suite2 = createMockTestDataItem(TestType.testSuite); - const suite3 = createMockTestDataItem(TestType.testSuite); - const suite4 = createMockTestDataItem(TestType.testSuite); - const suite5 = createMockTestDataItem(TestType.testSuite); - const fn1 = createMockTestDataItem(TestType.testFunction); - const fn2 = createMockTestDataItem(TestType.testFunction); - const fn3 = createMockTestDataItem(TestType.testFunction); - const fn4 = createMockTestDataItem(TestType.testFunction); - const fn5 = createMockTestDataItem(TestType.testFunction); + const file1 = createMockTestDataItem(TestDataItemType.file); + const file2 = createMockTestDataItem(TestDataItemType.file); + const file3 = createMockTestDataItem(TestDataItemType.file); + const file4 = createMockTestDataItem(TestDataItemType.file); + const suite1 = createMockTestDataItem(TestDataItemType.suite); + const suite2 = createMockTestDataItem(TestDataItemType.suite); + const suite3 = createMockTestDataItem(TestDataItemType.suite); + const suite4 = createMockTestDataItem(TestDataItemType.suite); + const suite5 = createMockTestDataItem(TestDataItemType.suite); + const fn1 = createMockTestDataItem(TestDataItemType.function); + const fn2 = createMockTestDataItem(TestDataItemType.function); + const fn3 = createMockTestDataItem(TestDataItemType.function); + const fn4 = createMockTestDataItem(TestDataItemType.function); + const fn5 = createMockTestDataItem(TestDataItemType.function); file1.suites.push(suite1); file1.suites.push(suite2); file3.suites.push(suite3); @@ -448,9 +448,9 @@ suite('Unit Tests - TestUtils', () => { assert.equal(getParentSuite(tests, suite5), suite4); }); test('Get Parent file throws an exception', () => { - const file1 = createMockTestDataItem(TestType.testFile); - const suite1 = createMockTestDataItem(TestType.testSuite); - const fn1 = createMockTestDataItem(TestType.testFunction); + const file1 = createMockTestDataItem(TestDataItemType.file); + const suite1 = createMockTestDataItem(TestDataItemType.suite); + const fn1 = createMockTestDataItem(TestDataItemType.function); const flattendSuite1: FlattenedTestSuite = { testSuite: suite1, xmlClassName: suite1.xmlName @@ -471,9 +471,9 @@ suite('Unit Tests - TestUtils', () => { assert.throws(() => getParentFile(tests, suite1), new RegExp('No parent file for provided test item')); }); test('Get parent of orphaned items', () => { - const file1 = createMockTestDataItem(TestType.testFile); - const suite1 = createMockTestDataItem(TestType.testSuite); - const fn1 = createMockTestDataItem(TestType.testFunction); + const file1 = createMockTestDataItem(TestDataItemType.file); + const suite1 = createMockTestDataItem(TestDataItemType.suite); + const fn1 = createMockTestDataItem(TestDataItemType.function); const flattendSuite1: FlattenedTestSuite = { testSuite: suite1, xmlClassName: suite1.xmlName @@ -494,15 +494,15 @@ suite('Unit Tests - TestUtils', () => { assert.equal(getParent(tests, suite1), undefined); }); test('Get Parent of suite', () => { - const file1 = createMockTestDataItem(TestType.testFile); - const file2 = createMockTestDataItem(TestType.testFile); - const file3 = createMockTestDataItem(TestType.testFile); - const file4 = createMockTestDataItem(TestType.testFile); - const suite1 = createMockTestDataItem(TestType.testSuite); - const suite2 = createMockTestDataItem(TestType.testSuite); - const suite3 = createMockTestDataItem(TestType.testSuite); - const suite4 = createMockTestDataItem(TestType.testSuite); - const suite5 = createMockTestDataItem(TestType.testSuite); + const file1 = createMockTestDataItem(TestDataItemType.file); + const file2 = createMockTestDataItem(TestDataItemType.file); + const file3 = createMockTestDataItem(TestDataItemType.file); + const file4 = createMockTestDataItem(TestDataItemType.file); + const suite1 = createMockTestDataItem(TestDataItemType.suite); + const suite2 = createMockTestDataItem(TestDataItemType.suite); + const suite3 = createMockTestDataItem(TestDataItemType.suite); + const suite4 = createMockTestDataItem(TestDataItemType.suite); + const suite5 = createMockTestDataItem(TestDataItemType.suite); file1.suites.push(suite1); file1.suites.push(suite2); file3.suites.push(suite3); @@ -543,20 +543,20 @@ suite('Unit Tests - TestUtils', () => { assert.equal(getParent(tests, suite5), suite4); }); test('Get Parent of function', () => { - const file1 = createMockTestDataItem(TestType.testFile); - const file2 = createMockTestDataItem(TestType.testFile); - const file3 = createMockTestDataItem(TestType.testFile); - const file4 = createMockTestDataItem(TestType.testFile); - const suite1 = createMockTestDataItem(TestType.testSuite); - const suite2 = createMockTestDataItem(TestType.testSuite); - const suite3 = createMockTestDataItem(TestType.testSuite); - const suite4 = createMockTestDataItem(TestType.testSuite); - const suite5 = createMockTestDataItem(TestType.testSuite); - const fn1 = createMockTestDataItem(TestType.testFunction); - const fn2 = createMockTestDataItem(TestType.testFunction); - const fn3 = createMockTestDataItem(TestType.testFunction); - const fn4 = createMockTestDataItem(TestType.testFunction); - const fn5 = createMockTestDataItem(TestType.testFunction); + const file1 = createMockTestDataItem(TestDataItemType.file); + const file2 = createMockTestDataItem(TestDataItemType.file); + const file3 = createMockTestDataItem(TestDataItemType.file); + const file4 = createMockTestDataItem(TestDataItemType.file); + const suite1 = createMockTestDataItem(TestDataItemType.suite); + const suite2 = createMockTestDataItem(TestDataItemType.suite); + const suite3 = createMockTestDataItem(TestDataItemType.suite); + const suite4 = createMockTestDataItem(TestDataItemType.suite); + const suite5 = createMockTestDataItem(TestDataItemType.suite); + const fn1 = createMockTestDataItem(TestDataItemType.function); + const fn2 = createMockTestDataItem(TestDataItemType.function); + const fn3 = createMockTestDataItem(TestDataItemType.function); + const fn4 = createMockTestDataItem(TestDataItemType.function); + const fn5 = createMockTestDataItem(TestDataItemType.function); file1.suites.push(suite1); file1.suites.push(suite2); file3.suites.push(suite3); @@ -622,16 +622,16 @@ suite('Unit Tests - TestUtils', () => { assert.equal(getParent(tests, fn5), suite3); }); test('Get parent of parameterized function', () => { - const folder = createMockTestDataItem(TestType.testFolder); - const file = createMockTestDataItem(TestType.testFile); - const func1 = createMockTestDataItem(TestType.testFunction); - const func2 = createMockTestDataItem(TestType.testFunction); - const func3 = createMockTestDataItem(TestType.testFunction); + const folder = createMockTestDataItem(TestDataItemType.folder); + const file = createMockTestDataItem(TestDataItemType.file); + const func1 = createMockTestDataItem(TestDataItemType.function); + const func2 = createMockTestDataItem(TestDataItemType.function); + const func3 = createMockTestDataItem(TestDataItemType.function); const subParent1 = createSubtestParent([func2, func3]); - const suite = createMockTestDataItem(TestType.testSuite); - const func4 = createMockTestDataItem(TestType.testFunction); - const func5 = createMockTestDataItem(TestType.testFunction); - const func6 = createMockTestDataItem(TestType.testFunction); + const suite = createMockTestDataItem(TestDataItemType.suite); + const func4 = createMockTestDataItem(TestDataItemType.function); + const func5 = createMockTestDataItem(TestDataItemType.function); + const func6 = createMockTestDataItem(TestDataItemType.function); const subParent2 = createSubtestParent([func5, func6]); folder.testFiles.push(file); file.functions.push(func1); @@ -662,16 +662,16 @@ suite('Unit Tests - TestUtils', () => { }); test('Get children of parameterized function', () => { const filename = path.join('tests', 'test_spam.py'); - const folder = createMockTestDataItem(TestType.testFolder, 'tests'); - const file = createMockTestDataItem(TestType.testFile, filename); - const func1 = createMockTestDataItem(TestType.testFunction, 'test_x'); - const func2 = createMockTestDataItem(TestType.testFunction, 'test_y'); - const func3 = createMockTestDataItem(TestType.testFunction, 'test_z'); + const folder = createMockTestDataItem(TestDataItemType.folder, 'tests'); + const file = createMockTestDataItem(TestDataItemType.file, filename); + const func1 = createMockTestDataItem(TestDataItemType.function, 'test_x'); + const func2 = createMockTestDataItem(TestDataItemType.function, 'test_y'); + const func3 = createMockTestDataItem(TestDataItemType.function, 'test_z'); const subParent1 = createSubtestParent([func2, func3]); - const suite = createMockTestDataItem(TestType.testSuite); - const func4 = createMockTestDataItem(TestType.testFunction); - const func5 = createMockTestDataItem(TestType.testFunction); - const func6 = createMockTestDataItem(TestType.testFunction); + const suite = createMockTestDataItem(TestDataItemType.suite); + const func4 = createMockTestDataItem(TestDataItemType.function); + const func5 = createMockTestDataItem(TestDataItemType.function); + const func6 = createMockTestDataItem(TestDataItemType.function); const subParent2 = createSubtestParent([func5, func6]); folder.testFiles.push(file); file.functions.push(func1); diff --git a/src/test/testing/common/xUnitParser.unit.test.ts b/src/test/testing/common/xUnitParser.unit.test.ts new file mode 100644 index 000000000000..9a9b5498b4ca --- /dev/null +++ b/src/test/testing/common/xUnitParser.unit.test.ts @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:max-func-body-length + +'use strict'; + +import { expect } from 'chai'; +import * as typeMoq from 'typemoq'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { + IXUnitParser, Tests, TestStatus +} from '../../../client/testing/common/types'; +import { XUnitParser } from '../../../client/testing/common/xUnitParser'; +import { createDeclaratively, createEmptyResults, TestItem } from '../results'; + +suite('Testing - parse JUnit XML file', () => { + let parser: IXUnitParser; + let fs: typeMoq.IMock; + setup(() => { + fs = typeMoq.Mock.ofType(undefined, typeMoq.MockBehavior.Strict); + parser = new XUnitParser(fs.object); + }); + + function fixResult(node: TestItem, file: string, line: number) { + switch (node.status) { + case TestStatus.Pass: + node.passed = true; + break; + case TestStatus.Fail: + case TestStatus.Error: + node.passed = false; + break; + default: + node.passed = undefined; + } + node.file = file; + node.line = line; + } + + test('success with single passing test', async () => { + const tests = createDeclaratively(` + ./ + test_spam.py + + test_spam + `); + const expected = createDeclaratively(` + ./ + test_spam.py + + test_spam P 1.001 + `); + fixResult( + expected.testFunctions[0].testFunction, + 'test_spam.py', + 3 + ); + const filename = 'x/y/z/results.xml'; + fs.setup(f => f.readFile(filename)) + .returns(() => Promise.resolve(` + + + + + + `)); + + await parser.updateResultsFromXmlLogFile(tests, filename); + + expect(tests).to.deep.equal(expected); + fs.verifyAll(); + }); + + test('no discovered tests', async () => { + const tests: Tests = createEmptyResults(); + const expected: Tests = createEmptyResults(); + expected.summary.passed = 1; // That's a little strange... + const filename = 'x/y/z/results.xml'; + fs.setup(f => f.readFile(filename)) + .returns(() => Promise.resolve(` + + + + + + `)); + + await parser.updateResultsFromXmlLogFile(tests, filename); + + expect(tests).to.deep.equal(expected); + fs.verifyAll(); + }); + + test('no tests run', async () => { + const tests: Tests = createEmptyResults(); + const expected: Tests = createEmptyResults(); + const filename = 'x/y/z/results.xml'; + fs.setup(f => f.readFile(filename)) + .returns(() => Promise.resolve(` + + + + `)); + + await parser.updateResultsFromXmlLogFile(tests, filename); + + expect(tests).to.deep.equal(expected); + fs.verifyAll(); + }); + + // Missing tests (see gh-7447): + // * simple pytest + // * simple nose + // * complex + // * error + // * failure + // * skipped + // * no clobber old if not matching + // * ... +}); diff --git a/src/test/testing/explorer/testTreeViewItem.unit.test.ts b/src/test/testing/explorer/testTreeViewItem.unit.test.ts index 569abf9e1f1b..e8521e62865e 100644 --- a/src/test/testing/explorer/testTreeViewItem.unit.test.ts +++ b/src/test/testing/explorer/testTreeViewItem.unit.test.ts @@ -9,12 +9,10 @@ import { Commands } from '../../../client/common/constants'; import { - TestFile, TestFolder, - TestFunction, TestSuite, TestType + TestFile, TestFolder, TestFunction, TestSuite } from '../../../client/testing/common/types'; -import { - TestTreeItem -} from '../../../client/testing/explorer/testTreeViewItem'; +import { TestTreeItem } from '../../../client/testing/explorer/testTreeViewItem'; +import { TestDataItemType } from '../../../client/testing/types'; import { createMockTestDataItem, createSubtestParent } from '../common/testUtils.unit.test'; @@ -33,38 +31,38 @@ suite('Unit Tests Test Explorer View Items', () => { test('Test root folder created into test view item', () => { const viewItem = new TestTreeItem(resource, testFolder); - expect(viewItem.contextValue).is.equal('testFolder'); + expect(viewItem.contextValue).is.equal('folder'); }); test('Test file created into test view item', () => { const viewItem = new TestTreeItem(resource, testFile); - expect(viewItem.contextValue).is.equal('testFile'); + expect(viewItem.contextValue).is.equal('file'); }); test('Test suite created into test view item', () => { const viewItem = new TestTreeItem(resource, testSuite); - expect(viewItem.contextValue).is.equal('testSuite'); + expect(viewItem.contextValue).is.equal('suite'); }); test('Test function created into test view item', () => { const viewItem = new TestTreeItem(resource, testFunction); - expect(viewItem.contextValue).is.equal('testFunction'); + expect(viewItem.contextValue).is.equal('function'); }); test('Test suite function created into test view item', () => { const viewItem = new TestTreeItem(resource, testSuiteFunction); - expect(viewItem.contextValue).is.equal('testFunction'); + expect(viewItem.contextValue).is.equal('function'); }); test('Test subtest parent created into test view item', () => { const subtestParent = createSubtestParent([ - createMockTestDataItem(TestType.testFunction, 'test_x'), - createMockTestDataItem(TestType.testFunction, 'test_y') + createMockTestDataItem(TestDataItemType.function, 'test_x'), + createMockTestDataItem(TestDataItemType.function, 'test_y') ]); const viewItem = new TestTreeItem(resource, subtestParent.asSuite); - expect(viewItem.contextValue).is.equal('testSuite'); + expect(viewItem.contextValue).is.equal('suite'); expect(viewItem.command!.command).is.equal(Commands.navigateToTestFunction); }); @@ -73,6 +71,6 @@ suite('Unit Tests Test Explorer View Items', () => { const viewItem = new TestTreeItem(resource, testFunction); - expect(viewItem.contextValue).is.equal('testFunction'); + expect(viewItem.contextValue).is.equal('function'); }); }); diff --git a/src/test/testing/explorer/testTreeViewProvider.unit.test.ts b/src/test/testing/explorer/testTreeViewProvider.unit.test.ts index b797186c06d5..91b5e9887041 100644 --- a/src/test/testing/explorer/testTreeViewProvider.unit.test.ts +++ b/src/test/testing/explorer/testTreeViewProvider.unit.test.ts @@ -14,14 +14,14 @@ import { Commands } from '../../../client/common/constants'; import { IDisposable } from '../../../client/common/types'; import { CommandSource } from '../../../client/testing/common/constants'; import { TestCollectionStorageService } from '../../../client/testing/common/services/storageService'; -import { getTestType } from '../../../client/testing/common/testUtils'; +import { getTestDataItemType } from '../../../client/testing/common/testUtils'; import { - ITestCollectionStorageService, TestFile, TestFolder, Tests, TestStatus, TestType + ITestCollectionStorageService, TestFile, TestFolder, Tests, TestStatus } from '../../../client/testing/common/types'; import { TestTreeItem } from '../../../client/testing/explorer/testTreeViewItem'; import { TestTreeViewProvider } from '../../../client/testing/explorer/testTreeViewProvider'; import { UnitTestManagementService } from '../../../client/testing/main'; -import { TestDataItem, TestWorkspaceFolder } from '../../../client/testing/types'; +import { TestDataItem, TestDataItemType, TestWorkspaceFolder } from '../../../client/testing/types'; import { noop } from '../../core'; import { createMockTestExplorer as createMockTestTreeProvider, createMockTestsData, @@ -296,22 +296,22 @@ suite('Unit Tests Test Explorer TestTreeViewProvider', () => { let parent = (await testTreeProvider.getParent(testFunction))!; expect(parent.name).to.be.equal(testSuite.name, 'Function within a test suite not returning the suite as parent.'); - let parentType = getTestType(parent); - expect(parentType).to.be.equal(TestType.testSuite); + let parentType = getTestDataItemType(parent); + expect(parentType).to.be.equal(TestDataItemType.suite); parent = (await testTreeProvider.getParent(testSuite))!; expect(parent.name).to.be.equal(testFile.name, 'Suite within a test file not returning the test file as parent.'); - parentType = getTestType(parent); - expect(parentType).to.be.equal(TestType.testFile); + parentType = getTestDataItemType(parent); + expect(parentType).to.be.equal(TestDataItemType.file); parent = (await testTreeProvider.getParent(outerTestFunction))!; expect(parent.name).to.be.equal(testFile.name, 'Function within a test file not returning the test file as parent.'); - parentType = getTestType(parent); - expect(parentType).to.be.equal(TestType.testFile); + parentType = getTestDataItemType(parent); + expect(parentType).to.be.equal(TestDataItemType.file); parent = (await testTreeProvider.getParent(testFile))!; - parentType = getTestType(parent!); - expect(parentType).to.be.equal(TestType.testFolder); + parentType = getTestDataItemType(parent!); + expect(parentType).to.be.equal(TestDataItemType.folder); }); test('Get children is working for each item type', async () => { @@ -334,7 +334,7 @@ suite('Unit Tests Test Explorer TestTreeViewProvider', () => { expect(children.length).to.be.equal(1, 'Suite a single function should only return one child.'); children.forEach((child: TestDataItem) => { expect(child.name).oneOf(['test_fn']); - expect(getTestType(child)).to.be.equal(TestType.testFunction); + expect(getTestDataItemType(child)).to.be.equal(TestDataItemType.function); }); children = await testTreeProvider.getChildren(outerTestFunction); diff --git a/src/test/testing/helper.ts b/src/test/testing/helper.ts index 846fa70dc386..9f4a75c405cb 100644 --- a/src/test/testing/helper.ts +++ b/src/test/testing/helper.ts @@ -3,9 +3,12 @@ import * as assert from 'assert'; import { sep } from 'path'; +import { Uri } from 'vscode'; import { IS_WINDOWS } from '../../client/common/platform/constants'; import { Tests } from '../../client/testing/common/types'; +export const RESOURCE = Uri.file(__filename); + export function lookForTestFile(tests: Tests, testFile: string) { let found: boolean; // Perform case insensitive search on windows. @@ -19,3 +22,53 @@ export function lookForTestFile(tests: Tests, testFile: string) { } assert.equal(found, true, `Test File not found '${testFile}'`); } + +// Return a filename that uses the OS-specific path separator. +// +// Only "/" (forward slash) in the given filename is affected. +// +// This helps with readability in test code. It allows us to use +// literals for filenames and dirnames instead of oath.join(). +export function fixPath(filename: string): string { + return filename.replace(/\//, sep); +} + +// Return the indentation part of the given line. +export function getIndent(line: string): string { + const found = line.match(/^ */); + return found![0]; +} + +// Return the dedented lines in the given text. +// +// This is used to represent text concisely and readably, which is +// particularly useful for declarative definitions (e.g. in tests). +// +// (inspired by Python's textwrap.dedent()) +export function getDedentedLines(text: string): string[] { + const linesep = text.includes('\r') ? '\r\n' : '\n'; + const lines = text.split(linesep); + if (!lines) { + return [text]; + } + + if (lines[0] !== '') { + throw Error('expected actual first line to be blank'); + } + lines.shift(); + + if (lines[0] === '') { + throw Error('expected "first" line to not be blank'); + } + const leading = getIndent(lines[0]).length; + + for (let i = 0; i < lines.length; i += 1) { + const line = lines[i]; + if (getIndent(line).length < leading) { + throw Error(`line ${i} has less indent than the "first" line`); + } + lines[i] = line.substring(leading); + } + + return lines; +} diff --git a/src/test/testing/pytest/pytest.testMessageService.test.ts b/src/test/testing/pytest/pytest.testMessageService.test.ts index 69be5d7d624d..a745a8a0c0d2 100644 --- a/src/test/testing/pytest/pytest.testMessageService.test.ts +++ b/src/test/testing/pytest/pytest.testMessageService.test.ts @@ -12,6 +12,8 @@ import * as vscode from 'vscode'; import { IWorkspaceService } from '../../../client/common/application/types'; import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; import { ProductNames } from '../../../client/common/installer/productNames'; +import { FileSystem } from '../../../client/common/platform/fileSystem'; +import { PlatformService } from '../../../client/common/platform/platformService'; import { Product } from '../../../client/common/types'; import { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; import { InterpreterService } from '../../../client/interpreter/interpreterService'; @@ -19,7 +21,7 @@ import { CondaService } from '../../../client/interpreter/locators/services/cond import { TestDiscoveredTestParser } from '../../../client/testing/common/services/discoveredTestParser'; import { TestResultsService } from '../../../client/testing/common/services/testResultsService'; import { DiscoveredTests } from '../../../client/testing/common/services/types'; -import { ITestVisitor, PassCalculationFormulae, TestDiscoveryOptions, Tests, TestStatus } from '../../../client/testing/common/types'; +import { ITestVisitor, TestDiscoveryOptions, Tests, TestStatus } from '../../../client/testing/common/types'; import { XUnitParser } from '../../../client/testing/common/xUnitParser'; import { TestMessageService } from '../../../client/testing/pytest/services/testMessageService'; import { ILocationStackFrameDetails, IPythonTestMessage, PythonTestMessageSeverity } from '../../../client/testing/types'; @@ -99,6 +101,8 @@ async function getExpectedLocationStackFromTestDetails(testDetails: ITestDetails suite('Unit Tests - PyTest - TestMessageService', () => { let ioc: UnitTestIocContainer; + const platformService = new PlatformService(); + const filesystem = new FileSystem(platformService); const configTarget = IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace; suiteSetup(async () => { await initialize(); @@ -141,8 +145,8 @@ suite('Unit Tests - PyTest - TestMessageService', () => { const discoveredTest: DiscoveredTests[] = JSON.parse(discoveryOutput); options.workspaceFolder = vscode.Uri.file(discoveredTest[0].root); const parsedTests: Tests = parser.parse(options.workspaceFolder, discoveredTest); - const xUnitParser = new XUnitParser(); - await xUnitParser.updateResultsFromXmlLogFile(parsedTests, path.join(PYTEST_RESULTS_PATH, scenario.runOutput), PassCalculationFormulae.pytest); + const xUnitParser = new XUnitParser(filesystem); + await xUnitParser.updateResultsFromXmlLogFile(parsedTests, path.join(PYTEST_RESULTS_PATH, scenario.runOutput)); const testResultsService = new TestResultsService(testVisitor.object); testResultsService.updateResults(parsedTests); const testMessageService = new TestMessageService(ioc.serviceContainer); diff --git a/src/test/testing/results.ts b/src/test/testing/results.ts new file mode 100644 index 000000000000..136555c58758 --- /dev/null +++ b/src/test/testing/results.ts @@ -0,0 +1,635 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { Uri } from 'vscode'; +import { + FlattenedTestFunction, FlattenedTestSuite, + SubtestParent, TestFile, TestFolder, TestFunction, TestingType, + TestProvider, TestResult, Tests, TestStatus, TestSuite, TestSummary +} from '../../client/testing/common/types'; +import { fixPath, getDedentedLines, getIndent, RESOURCE } from './helper'; + +type SuperTest = TestFunction & { + subtests: TestFunction[]; +}; + +export type TestItem = TestFolder | TestFile | TestSuite | SuperTest | TestFunction; + +export type TestNode = TestItem & { + testType: TestingType; +}; + +// Return an initialized test results. +export function createEmptyResults(): Tests { + return { + summary: { + passed: 0, + failures: 0, + errors: 0, + skipped: 0 + }, + testFiles: [], + testFunctions: [], + testSuites: [], + testFolders: [], + rootTestFolders: [] + }; +} + +// Increment the appropriate summary property. +export function updateSummary( + summary: TestSummary, + status: TestStatus +) { + switch (status) { + case TestStatus.Pass: + summary.passed += 1; + break; + case TestStatus.Fail: + summary.failures += 1; + break; + case TestStatus.Error: + summary.errors += 1; + break; + case TestStatus.Skipped: + summary.skipped += 1; + break; + default: + // Do not update the results. + } +} + +// Return the file found walking up the parents, if any. +// +// There should only be one parent file. +export function findParentFile(parents: TestNode[]): TestFile | undefined { + // Iterate in reverse order. + for (let i = parents.length; i > 0; i -= 1) { + const parent = parents[i - 1]; + if (parent.testType === TestingType.file) { + return parent as TestFile; + } + } + return; +} + +// Return the first suite found walking up the parents, if any. +export function findParentSuite(parents: TestNode[]): TestSuite | undefined { + // Iterate in reverse order. + for (let i = parents.length; i > 0; i -= 1) { + const parent = parents[i - 1]; + if (parent.testType === TestingType.suite) { + return parent as TestSuite; + } + } + return; +} + +// Return the "flattened" test suite node. +export function flattenSuite( + node: TestSuite, + parents: TestNode[] +): FlattenedTestSuite { + const found = findParentFile(parents); + if (!found) { + throw Error('parent file not found'); + } + const parentFile: TestFile = found; + return { + testSuite: node, + parentTestFile: parentFile, + xmlClassName: node.xmlName + }; +} + +// Return the "flattened" test function node. +export function flattenFunction( + node: TestFunction, + parents: TestNode[] +): FlattenedTestFunction { + const found = findParentFile(parents); + if (!found) { + throw Error('parent file not found'); + } + const parentFile: TestFile = found; + const parentSuite = findParentSuite(parents); + return { + testFunction: node, + parentTestFile: parentFile, + parentTestSuite: parentSuite, + xmlClassName: parentSuite ? parentSuite.xmlName : '' + }; +} + +// operations on raw test nodes +export namespace nodes { + // Set the result-oriented properties back to their "unset" values. + export function resetResult(node: TestNode) { + node.time = 0; + node.status = TestStatus.Unknown; + } + + //******************************** + // builders for empty low-level test results + + export function createFolderResults( + dirname: string, + nameToRun?: string, + resource: Uri = RESOURCE + ): TestNode { + dirname = fixPath(dirname); + return { + resource: resource, + name: dirname, + nameToRun: nameToRun || dirname, + folders: [], + testFiles: [], + testType: TestingType.folder, + // result + time: 0, + status: TestStatus.Unknown + }; + } + + export function createFileResults( + filename: string, + nameToRun?: string, + xmlName?: string, + resource: Uri = RESOURCE + ): TestNode { + filename = fixPath(filename); + if (!xmlName) { + xmlName = filename + .replace(/\.[^.]+$/, '') + .replace(/[\\\/]/, '.') + .replace(/^[.\\\/]*/, ''); + } + return { + resource: resource, + fullPath: filename, + name: path.basename(filename), + nameToRun: nameToRun || filename, + xmlName: xmlName!, + suites: [], + functions: [], + testType: TestingType.file, + // result + time: 0, + status: TestStatus.Unknown + }; + } + + export function createSuiteResults( + name: string, + nameToRun?: string, + xmlName?: string, + provider: TestProvider = 'pytest', + isInstance: boolean = false, + resource: Uri = RESOURCE + ): TestNode { + return { + resource: resource, + name: name, + nameToRun: nameToRun || '', // must be set for parent + xmlName: xmlName || '', // must be set for parent + isUnitTest: provider === 'unittest', + isInstance: isInstance, + suites: [], + functions: [], + testType: TestingType.suite, + // result + time: 0, + status: TestStatus.Unknown + }; + } + + export function createTestResults( + name: string, + nameToRun?: string, + subtestParent?: SubtestParent, + resource: Uri = RESOURCE + ): TestNode { + return { + resource: resource, + name: name, + nameToRun: nameToRun || name, + subtestParent: subtestParent, + testType: TestingType.function, + // result + time: 0, + status: TestStatus.Unknown + }; + } + + //******************************** + // adding children to low-level nodes + + export function addDiscoveredSubFolder( + parent: TestFolder, + basename: string, + nameToRun?: string, + resource?: Uri + ): TestNode { + const dirname = path.join(parent.name, fixPath(basename)); + const subFolder = createFolderResults( + dirname, + nameToRun, + resource || parent.resource || RESOURCE + ); + parent.folders.push(subFolder as TestFolder); + return subFolder; + } + + export function addDiscoveredFile( + parent: TestFolder, + basename: string, + nameToRun?: string, + xmlName?: string, + resource?: Uri + ): TestNode { + const filename = path.join(parent.name, fixPath(basename)); + const file = createFileResults( + filename, + nameToRun, + xmlName, + resource || parent.resource || RESOURCE + ); + parent.testFiles.push(file as TestFile); + return file; + } + + export function addDiscoveredSuite( + parent: TestFile | TestSuite, + name: string, + nameToRun?: string, + xmlName?: string, + provider: TestProvider = 'pytest', + isInstance?: boolean, + resource?: Uri + ): TestNode { + if (!nameToRun) { + const sep = provider === 'pytest' ? '::' : '.'; + nameToRun = `${parent.nameToRun}${sep}${name}`; + } + const suite = createSuiteResults( + name, + nameToRun!, + xmlName || `${parent.xmlName}.${name}`, + provider, + isInstance, + resource || parent.resource || RESOURCE + ); + parent.suites.push(suite as TestSuite); + return suite; + } + + export function addDiscoveredTest( + parent: TestFile | TestSuite, + name: string, + nameToRun?: string, + provider: TestProvider = 'pytest', + resource?: Uri + ): TestNode { + if (!nameToRun) { + const sep = provider === 'pytest' ? '::' : '.'; + nameToRun = `${parent.nameToRun}${sep}${name}`; + } + const test = createTestResults( + name, + nameToRun, + undefined, + resource || parent.resource || RESOURCE + ); + parent.functions.push(test as TestFunction); + return test; + } + + export function addDiscoveredSubtest( + parent: SuperTest, + name: string, + nameToRun?: string, + provider: TestProvider = 'pytest', + resource?: Uri + ): TestNode { + const subtest = createTestResults( + name, + nameToRun!, + { + name: parent.name, + nameToRun: parent.nameToRun, + asSuite: createSuiteResults( + parent.name, + parent.nameToRun, + '', + provider, + false, + parent.resource + ) as TestSuite, + time: 0 + }, + resource || parent.resource || RESOURCE + ); + (subtest as TestFunction).subtestParent!.asSuite.functions.push(subtest); + parent.subtests.push(subtest as TestFunction); + return subtest; + } +} + +namespace declarative { + type TestParent = TestNode & { + indent: string; + }; + + type ParsedTestNode = { + indent: string; + name: string; + testType: TestingType; + result: TestResult; + }; + + // Return a test tree built from concise declarative text. + export function parseResults( + text: string, + tests: Tests, + provider: TestProvider, + resource: Uri + ) { + // Build the tree (and populate the return value at the same time). + const parents: TestParent[] = []; + let prev: TestParent; + for (const line of getDedentedLines(text)) { + if (line.trim() === '') { + continue; + } + const parsed = parseTestLine(line); + + let node: TestNode; + if (isRootNode(parsed)) { + parents.length = 0; // Clear the array. + node = nodes.createFolderResults( + parsed.name, + undefined, + resource + ); + tests.rootTestFolders.push(node as TestFolder); + tests.testFolders.push(node as TestFolder); + } else { + const parent = setMatchingParent( + parents, + prev!, + parsed.indent + ); + node = buildDiscoveredChildNode( + parent, + parsed.name, + parsed.testType, + provider, + resource + ); + switch (parsed.testType) { + case TestingType.folder: + tests.testFolders.push(node as TestFolder); + break; + case TestingType.file: + tests.testFiles.push(node as TestFile); + break; + case TestingType.suite: + tests.testSuites.push( + flattenSuite(node as TestSuite, parents) + ); + break; + case TestingType.function: + // This does not deal with subtests? + tests.testFunctions.push( + flattenFunction(node as TestFunction, parents) + ); + break; + default: + } + } + + // Set the result. + node.status = parsed.result.status; + node.time = parsed.result.time; + updateSummary(tests.summary, node.status!); + + // Prepare for the next line. + prev = node as TestParent; + prev.indent = parsed.indent; + } + } + + // Determine the kind, indent, and result info based on the line. + function parseTestLine(line: string): ParsedTestNode { + if (line.includes('\\')) { + throw Error('expected / as path separator (even on Windows)'); + } + + const indent = getIndent(line); + line = line.trim(); + + const parts = line.split(' '); + let name = parts.shift(); + if (!name) { + throw Error('missing name'); + } + + // Determine the type from the name. + let testType: TestingType; + if (name.endsWith('/')) { + // folder + testType = TestingType.folder; + while (name.endsWith('/')) { + name = name.slice(0, -1); + } + } else if (name.includes('.')) { + // file + if (name.includes('/')) { + throw Error('filename must not include directories'); + } + testType = TestingType.file; + } else if (name.startsWith('<')) { + // suite + if (!name.endsWith('>')) { + throw Error('suite missing closing bracket'); + } + testType = TestingType.suite; + name = name.slice(1, -1); + } else { + // test + testType = TestingType.function; + } + + // Parse the results. + const result: TestResult = { + time: 0 + }; + if (parts.length !== 0 && testType !== TestingType.function) { + throw Error('non-test nodes do not have results'); + } + switch (parts.length) { + case 0: + break; + case 1: + // tslint:disable-next-line:no-any + if (isNaN(parts[0] as any)) { + throw Error(`expected a time (float), got ${parts[0]}`); + } + result.time = parseFloat(parts[0]); + break; + case 2: + switch (parts[0]) { + case 'P': + result.status = TestStatus.Pass; + break; + case 'F': + result.status = TestStatus.Fail; + break; + case 'E': + result.status = TestStatus.Error; + break; + case 'S': + result.status = TestStatus.Skipped; + break; + default: + throw Error('expected a status and then a time'); + } + // tslint:disable-next-line:no-any + if (isNaN(parts[1] as any)) { + throw Error(`expected a time (float), got ${parts[1]}`); + } + result.time = parseFloat(parts[1]); + break; + default: + throw Error('too many items on line'); + } + + return { + indent: indent, + name: name, + testType: testType, + result: result + }; + } + + function isRootNode( + parsed: ParsedTestNode + ): boolean { + if (parsed.indent === '') { + if (parsed.testType !== TestingType.folder) { + throw Error('a top-level node must be a folder'); + } + return true; + } + return false; + } + + function setMatchingParent( + parents: TestParent[], + prev: TestParent, + parsedIndent: string + ): TestParent { + let current = parents.length > 0 ? parents[parents.length - 1] : prev; + if (parsedIndent.length > current.indent.length) { + parents.push(prev); + current = prev; + } else { + while (parsedIndent !== current.indent) { + if (parsedIndent.length > current.indent.length) { + throw Error('mis-aligned indentation'); + } + + parents.pop(); + if (parents.length === 0) { + throw Error('mis-aligned indentation'); + } + current = parents[parents.length - 1]; + } + } + return current; + } + + function buildDiscoveredChildNode( + parent: TestParent, + name: string, + testType: TestingType, + provider: TestProvider, + resource?: Uri + ): TestNode { + switch (testType) { + case TestingType.folder: + if (parent.testType !== TestingType.folder) { + throw Error('parent must be a folder'); + } + return nodes.addDiscoveredSubFolder( + parent as TestFolder, + name, + undefined, + resource + ); + case TestingType.file: + if (parent.testType !== TestingType.folder) { + throw Error('parent must be a folder'); + } + return nodes.addDiscoveredFile( + parent as TestFolder, + name, + undefined, + undefined, + resource + ); + case TestingType.suite: + let suiteParent: TestFile | TestSuite; + if (parent.testType === TestingType.file) { + suiteParent = parent as TestFile; + } else if (parent.testType === TestingType.suite) { + suiteParent = parent as TestSuite; + } else { + throw Error('parent must be a file or suite'); + } + return nodes.addDiscoveredSuite( + suiteParent, + name, + undefined, + undefined, + provider, + undefined, + resource + ); + case TestingType.function: + let funcParent: TestFile | TestSuite; + if (parent.testType === TestingType.file) { + funcParent = parent as TestFile; + } else if (parent.testType === TestingType.suite) { + funcParent = parent as TestSuite; + } else if (parent.testType === TestingType.function) { + throw Error('not finished: use addDiscoveredSubTest()'); + } else { + throw Error('parent must be a file, suite, or function'); + } + return nodes.addDiscoveredTest( + funcParent, + name, + undefined, + provider, + resource + ); + default: + throw Error('unsupported'); + } + } +} + +// Return a test tree built from concise declarative text. +export function createDeclaratively( + text: string, + provider: TestProvider = 'pytest', + resource: Uri = RESOURCE +): Tests { + const tests = createEmptyResults(); + declarative.parseResults(text, tests, provider, resource); + return tests; +} diff --git a/src/test/testing/unittest/unittest.unit.test.ts b/src/test/testing/unittest/unittest.unit.test.ts index 7cafcc6b493f..6a762d5453fa 100644 --- a/src/test/testing/unittest/unittest.unit.test.ts +++ b/src/test/testing/unittest/unittest.unit.test.ts @@ -19,8 +19,14 @@ import { TestResultsService } from '../../../client/testing/common/services/test import { TestsStatusUpdaterService } from '../../../client/testing/common/services/testsStatusService'; import { TestsHelper } from '../../../client/testing/common/testUtils'; import { TestResultResetVisitor } from '../../../client/testing/common/testVisitors/resultResetVisitor'; -import { FlattenedTestFunction, FlattenedTestSuite, ITestResultsService, ITestsHelper, ITestsStatusUpdaterService, TestFile, TestFolder, TestFunction, Tests, TestStatus, TestSuite, TestType } from '../../../client/testing/common/types'; -import { IArgumentsHelper, IArgumentsService, ITestManagerRunner } from '../../../client/testing/types'; +import { + FlattenedTestFunction, FlattenedTestSuite, ITestResultsService, + ITestsHelper, ITestsStatusUpdaterService, TestFile, TestFolder, + TestFunction, Tests, TestStatus, TestSuite +} from '../../../client/testing/common/types'; +import { + IArgumentsHelper, IArgumentsService, ITestManagerRunner, TestDataItemType +} from '../../../client/testing/types'; import { TestManager } from '../../../client/testing/unittest/main'; import { TestManagerRunner } from '../../../client/testing/unittest/runner'; import { ArgumentsService } from '../../../client/testing/unittest/services/argsService'; @@ -37,35 +43,35 @@ suite('Unit Tests - unittest - run failed tests', () => { let tests: Tests; function createTestData() { - const folder1 = createMockTestDataItem(TestType.testFolder); - const folder2 = createMockTestDataItem(TestType.testFolder); - const folder3 = createMockTestDataItem(TestType.testFolder); - const folder4 = createMockTestDataItem(TestType.testFolder); - const folder5 = createMockTestDataItem(TestType.testFolder); + const folder1 = createMockTestDataItem(TestDataItemType.folder); + const folder2 = createMockTestDataItem(TestDataItemType.folder); + const folder3 = createMockTestDataItem(TestDataItemType.folder); + const folder4 = createMockTestDataItem(TestDataItemType.folder); + const folder5 = createMockTestDataItem(TestDataItemType.folder); folder1.folders.push(folder2); folder1.folders.push(folder3); folder2.folders.push(folder4); folder3.folders.push(folder5); - const file1 = createMockTestDataItem(TestType.testFile); - const file2 = createMockTestDataItem(TestType.testFile); - const file3 = createMockTestDataItem(TestType.testFile); - const file4 = createMockTestDataItem(TestType.testFile); + const file1 = createMockTestDataItem(TestDataItemType.file); + const file2 = createMockTestDataItem(TestDataItemType.file); + const file3 = createMockTestDataItem(TestDataItemType.file); + const file4 = createMockTestDataItem(TestDataItemType.file); folder1.testFiles.push(file1); folder3.testFiles.push(file2); folder3.testFiles.push(file3); folder5.testFiles.push(file4); - const suite1 = createMockTestDataItem(TestType.testSuite); - const suite2 = createMockTestDataItem(TestType.testSuite); - const suite3 = createMockTestDataItem(TestType.testSuite); - const suite4 = createMockTestDataItem(TestType.testSuite); - const suite5 = createMockTestDataItem(TestType.testSuite); - const fn1 = createMockTestDataItem(TestType.testFunction); - const fn2 = createMockTestDataItem(TestType.testFunction); - const fn3 = createMockTestDataItem(TestType.testFunction); - const fn4 = createMockTestDataItem(TestType.testFunction); - const fn5 = createMockTestDataItem(TestType.testFunction); + const suite1 = createMockTestDataItem(TestDataItemType.suite); + const suite2 = createMockTestDataItem(TestDataItemType.suite); + const suite3 = createMockTestDataItem(TestDataItemType.suite); + const suite4 = createMockTestDataItem(TestDataItemType.suite); + const suite5 = createMockTestDataItem(TestDataItemType.suite); + const fn1 = createMockTestDataItem(TestDataItemType.function); + const fn2 = createMockTestDataItem(TestDataItemType.function); + const fn3 = createMockTestDataItem(TestDataItemType.function); + const fn4 = createMockTestDataItem(TestDataItemType.function); + const fn5 = createMockTestDataItem(TestDataItemType.function); file1.suites.push(suite1); file1.suites.push(suite2); file3.suites.push(suite3);