import path from 'path' ; import { createHash } from 'crypto' ; import singletonAllureInstance from './singletonAllureInstance' ; import { AllureRuntime , IAllureConfig , ExecutableItemWrapper , AllureTest , AllureStep , Stage , Status , LabelName , AllureGroup } from 'allure-js-commons' ; import { hostname } from 'os' ; import { AllureWriter } from 'allure-js-commons/dist/src/writers' ; import { StepName } from './StepName' ; import { IStep , ITestReporter , IConfig } from './types' ; import { isPendingError } from './PendingError' ; class CompositeTestReporter implements ITestReporter { constructor ( public reporters ) { } onFinish ( errors ) { this . reporters . forEach ( reporter => reporter . onFinish ( errors ) ) ; } onEnterStep ( step ) { this . reporters . forEach ( reporter => reporter . onEnterStep ( step ) ) ; } onExitStep ( step , error ? ) { this . reporters . forEach ( reporter => reporter . onExitStep ( step , error ) ) ; } } class AllureTestReporter implements ITestReporter { private _stack : IStepStack ; private _group : AllureGroup ; constructor ( { suiteName , caseName , resultsDir } ) { const historyId = createHash ( 'md5' ) . update ( [ suiteName , caseName ] . join ( ' / ' ) ) . digest ( 'hex' ) ; const allureConfig = { resultsDir } ; const writer = new AllureWriter ( allureConfig ) ; const runtime = new AllureRuntime ( { ... allureConfig , writer } ) ; const group = runtime . startGroup ( suiteName ) ; const test = group . startTest ( caseName ) ; const prescriptVersion = require ( '../package' ) . version ; test . historyId = historyId ; test . addLabel ( LabelName . THREAD , `${ process . pid }` ) ; test . addLabel ( LabelName . HOST , `${ hostname ( ) }` ) ; test . addLabel ( LabelName . FRAMEWORK , `prescript@${ prescriptVersion }` ) ; for ( const [ key , value ] of Object . entries ( process . env ) ) { if ( key . startsWith ( 'ALLURE_ENV_' ) && value ) { test . addParameter ( key , value ) ; } } this . _stack = new TestStepStack ( test ) ; this . _group = group ; singletonAllureInstance . currentReportingInterface = { addAttachment : ( name , buf , mimeType ) => { const sha = createHash ( 'sha256' ) . update ( buf ) . digest ( 'hex' ) ; const fileName = sha + path . extname ( name ) ; writer . writeAttachment ( fileName , buf ) ; this . _stack . getExecutableItem ( ) . addAttachment ( name , mimeType as any , fileName ) ; } } ; } onEnterStep ( node ) { if ( ! node . number ) { return ; } this . _stack = this . _stack . push ( String ( node . name ) ) ; } onExitStep ( node , error ? ) { if ( ! node . number ) { return ; } this . _stack = this . _stack . pop ( error ) ; } onFinish ( errors ) { this . _stack = this . _stack . pop ( errors [ 0 ] ) ; this . _group . endGroup ( ) ; } } export default function createReporter ( testModulePath , rootStepName , customTestReporterFactory ) { const reporters = [ ] ; if ( process . env . ALLURE_SUITE_NAME || process . env . ALLURE_RESULTS_DIR || process . env . ALLURE_CASE_NAME ) { const suiteName = process . env . ALLURE_SUITE_NAME || 'prescript' ; const getDefaultCaseName = ( ) => { const testPath = path . relative ( process . cwd ( ) , testModulePath ) ; const rawTestName = String ( rootStepName ) ; const testName = rawTestName === '[implicit test]' ? '' : rawTestName ; return `${ testPath }${ testName ? ` - ${ testName }` : '' }` ; } ; const caseName = process . env . ALLURE_CASE_NAME || getDefaultCaseName ( ) ; const resultsDir = process . env . ALLURE_RESULTS_DIR || 'allure-results' ; reporters . push ( new AllureTestReporter ( { suiteName , caseName , resultsDir } ) ) ; } if ( customTestReporterFactory ) { reporters . push ( customTestReporterFactory ( testModulePath , String ( rootStepName ) ) ) ; } return new CompositeTestReporter ( reporters ) ; } type Outcome = Error | undefined ; interface IStepStack { push : ( stepName : string ) => IStepStack ; pop : ( outcome : Outcome ) => IStepStack ; getExecutableItem : ( ) => ExecutableItemWrapper ; } const saveOutcome = ( executableItem , outcome ) => { if ( ! outcome ) { executableItem . status = Status . PASSED ; executableItem . stage = Stage . FINISHED ; return ; } if ( isPendingError ( outcome ) ) { executableItem . stage = Stage . FINISHED ; executableItem . status = Status . SKIPPED ; return ; } executableItem . stage = Stage . FINISHED ; executableItem . status = Status . FAILED ; executableItem . detailsMessage = outcome . message || '' ; executableItem . detailsTrace = outcome . stack || '' ; } ; class NullStepStack implements IStepStack { push ( ) { throw new Error ( 'This should not happen: Allure stack is corrupted.' ) ; } pop ( ) { throw new Error ( 'This should not happen: Allure stack is corrupted.' ) ; } getExecutableItem ( ) { throw new Error ( 'This should not happen: Allure stack is corrupted.' ) ; } } class TestStepStack implements IStepStack { constructor ( private test ) { } push ( stepName ) { return new StepStepStack ( this , this . test . startStep ( stepName ) ) ; } pop ( outcome ) { saveOutcome ( this . test , outcome ) ; this . test . endTest ( ) ; return new NullStepStack ( ) ; } getExecutableItem ( ) { return this . test ; } } class StepStepStack implements IStepStack { constructor ( private parent , private step ) { } push ( stepName ) { return new StepStepStack ( this , this . step . startStep ( stepName ) ) ; } pop ( outcome ) { saveOutcome ( this . step , outcome ) ; this . step . endStep ( ) ; return this . parent ; } getExecutableItem ( ) { return this . step ; } }