import ArtifactFilePathFetcher , { ArtifactFilePaths , } from "@dxatscale/sfpowerscripts.core/lib/artifacts/ArtifactFilePathFetcher" ; import PackageMetadata from "@dxatscale/sfpowerscripts.core/lib/PackageMetadata" ; import ArtifactInstallationStatusChecker from "@dxatscale/sfpowerscripts.core/lib/artifacts/ArtifactInstallationStatusChecker" ; import InstalledAritfactsFetcher from "@dxatscale/sfpowerscripts.core/lib/artifacts/InstalledAritfactsFetcher" ; import ArtifactInquirer from "@dxatscale/sfpowerscripts.core/lib/artifacts/ArtifactInquirer" ; import fs = require ( "fs" ) ; import path = require ( "path" ) ; import SFPLogger , { Logger , LoggerLevel , } from "@dxatscale/sfpowerscripts.core/lib/logger/SFPLogger" ; import { EOL } from "os" ; import { Stage } from "../Stage" ; import ProjectConfig from "@dxatscale/sfpowerscripts.core/lib/project/ProjectConfig" ; import TriggerApexTests from "@dxatscale/sfpowerscripts.core/lib/sfpcommands/apextest/TriggerApexTests" ; import SFPPackage from "@dxatscale/sfpowerscripts.core/lib/package/SFPPackage" ; import { RunAllTestsInPackageOptions } from "@dxatscale/sfpowerscripts.core/lib/sfpcommands/apextest/ExtendedTestOptions" ; import { TestOptions } from "@dxatscale/sfpowerscripts.core/lib/sfdxwrappers/TestOptions" ; import semver = require ( "semver" ) ; import PromoteUnlockedPackageImpl from "@dxatscale/sfpowerscripts.core/lib/sfdxwrappers/PromoteUnlockedPackageImpl" ; import { DeploymentType } from "@dxatscale/sfpowerscripts.core/lib/sfpcommands/source/DeploymentExecutor" ; import { COLOR_ERROR } from "@dxatscale/sfpowerscripts.core/lib/logger/SFPLogger" ; import { COLOR_KEY_MESSAGE } from "@dxatscale/sfpowerscripts.core/lib/logger/SFPLogger" ; import { COLOR_HEADER } from "@dxatscale/sfpowerscripts.core/lib/logger/SFPLogger" ; import { CoverageOptions } from "@dxatscale/sfpowerscripts.core/lib/apex/coverage/IndividualClassCoverage" ; import { PackageInstallationResult , PackageInstallationStatus } from "@dxatscale/sfpowerscripts.core/lib/package/packageInstallers/PackageInstallationResult" ; import InstallUnlockedPackageImpl from "@dxatscale/sfpowerscripts.core/lib/package/packageInstallers/InstallUnlockedPackageImpl" ; import InstallSourcePackageImpl from "@dxatscale/sfpowerscripts.core/lib/package/packageInstallers/InstallSourcePackageImpl" ; import InstallDataPackageImpl from "@dxatscale/sfpowerscripts.core/lib/package/packageInstallers/InstallDataPackageImpl" ; const Table = require ( "cli-table" ) ; const retry = require ( "async-retry" ) ; export enum DeploymentMode { NORMAL , SOURCEPACKAGES , SOURCEPACKAGES_PUSH } export interface DeployProps { targetUsername : string ; artifactDir : string ; deploymentMode : DeploymentMode ; isTestsToBeTriggered : boolean ; skipIfPackageInstalled : boolean ; logsGroupSymbol ? : string [ ] ; coverageThreshold ? : number ; waitTime : number ; tags ? : any ; packageLogger ? : Logger ; currentStage ? : Stage ; baselineOrg ? : string ; isDryRun ? : boolean ; isRetryOnFailure ? : boolean ; promotePackagesBeforeDeploymentToOrg ? : string ; devhubUserName ? : string ; } export default class DeployImpl { constructor ( private props ) { } public async exec ( ) { let deployed = [ ] ; let failed = [ ] ; let testFailure ; let queue ; try { let artifacts = ArtifactFilePathFetcher . fetchArtifactFilePaths ( this . props . artifactDir , null , this . props . packageLogger ) ; if ( artifacts . length === 0 ) throw new Error ( `No artifacts to deploy found in ${ this . props . artifactDir }` ) ; let artifactInquirer = new ArtifactInquirer ( artifacts , this . props . packageLogger ) ; let packageManifest = artifactInquirer . latestPackageManifestFromArtifacts ; if ( packageManifest == null ) { packageManifest = ProjectConfig . getSFDXPackageManifest ( null ) ; } let packagesToPackageInfo = this . getPackagesToPackageInfo ( artifacts ) ; queue = this . getPackagesToDeploy ( packageManifest , packagesToPackageInfo ) ; if ( this . props . skipIfPackageInstalled ) { let isBaselinOrgModeActivated ; if ( this . props . baselineOrg ) { isBaselinOrgModeActivated = true ; } else { isBaselinOrgModeActivated = false ; this . props . baselineOrg = this . props . targetUsername ; } let filteredDeploymentQueue = await this . filterByPackagesInstalledInTheOrg ( packageManifest , queue , packagesToPackageInfo , this . props . baselineOrg ) ; this . printArtifactVersionsWhenSkipped ( queue , packagesToPackageInfo , isBaselinOrgModeActivated ) ; queue = filteredDeploymentQueue ; } else { this . printArtifactVersions ( queue , packagesToPackageInfo ) ; } for ( let i = 0 ; i < queue . length ; i ++ ) { let packageInfo = packagesToPackageInfo [ queue [ i ] . package ] ; let packageMetadata = packageInfo . packageMetadata ; let packageType = packageMetadata . package_type ; let pkgDescriptor = ProjectConfig . getPackageDescriptorFromConfig ( queue [ i ] . package , packageManifest ) ; this . printOpenLoggingGroup ( "Installing " , queue [ i ] . package ) ; this . displayHeader ( packageMetadata , pkgDescriptor , queue [ i ] . package ) ; let packageInstallationResult = await retry ( async ( bail , count ) => { try { await this . promotePackagesBeforeInstallation ( packageInfo . sourceDirectory , packageMetadata ) ; this . displayRetryHeader ( this . props . isRetryOnFailure , count ) ; let installPackageResult = await this . installPackage ( packageType , queue [ i ] . package , this . props . targetUsername , packageInfo . sourceDirectory , packageMetadata , queue [ i ] . skipTesting , this . props . waitTime . toString ( ) , pkgDescriptor , false ) ; if ( this . props . isRetryOnFailure && installPackageResult . result === PackageInstallationStatus . Failed && count === 1 ) { throw new Error ( installPackageResult . message ) ; } else return installPackageResult ; } catch ( error ) { if ( ! this . props . isRetryOnFailure ) { let failedPackageInstallationResult = { result : PackageInstallationStatus . Failed , message : error } ; return failedPackageInstallationResult ; } else throw ( error ) ; } } , { retries : 1 , minTimeout : 2000 } ) ; if ( packageInstallationResult . result === PackageInstallationStatus . Succeeded ) { deployed . push ( packageInfo ) ; this . printClosingLoggingGroup ( ) ; } else if ( packageInstallationResult . result === PackageInstallationStatus . Skipped ) { this . printClosingLoggingGroup ( ) ; continue ; } else if ( packageInstallationResult . result === PackageInstallationStatus . Failed ) { failed = queue . slice ( i ) . map ( ( pkg ) => packagesToPackageInfo [ pkg . package ] ) ; throw new Error ( packageInstallationResult . message ) ; } if ( this . props . isTestsToBeTriggered ) { if ( packageMetadata . isApexFound ) { if ( ! queue [ i ] . skipTesting ) { this . printOpenLoggingGroup ( "Trigger Tests for " , queue [ i ] . package ) ; let testResult ; try { testResult = await this . triggerApexTests ( queue [ i ] . package , this . props . targetUsername , queue [ i ] . skipCoverageValidation , this . props . coverageThreshold ) ; } catch ( error ) { console . log ( COLOR_ERROR ( error . message ) ) ; testResult = { result : false , message : "Test Execution failed" } ; } if ( ! testResult . result ) { testFailure = packageInfo ; if ( i !== queue . length - 1 ) failed = queue . slice ( i + 1 ) . map ( ( pkg ) => packagesToPackageInfo [ pkg . package ] ) ; throw new Error ( testResult . message ) ; } else { SFPLogger . log ( testResult . message , LoggerLevel . INFO , this . props . packageLogger ) ; this . printClosingLoggingGroup ( ) ; } } else { SFPLogger . log ( `Skipping testing of ${ queue [ i ] . package }\n` , LoggerLevel . INFO , this . props . packageLogger ) ; } } } } return { scheduled : queue . length , deployed : deployed , failed : failed , testFailure : testFailure , error : null , } ; } catch ( err ) { SFPLogger . log ( err , LoggerLevel . ERROR , this . props . packageLogger ) ; return { scheduled : queue ?. length ? queue . length : 0 , deployed : deployed , failed : failed , testFailure : testFailure , error : err , } ; } } private async promotePackagesBeforeInstallation ( sourceDirectory , packageMetadata ) { if ( this . props . promotePackagesBeforeDeploymentToOrg === this . props . targetUsername ) { if ( packageMetadata . package_type === 'unlocked' ) { console . log ( COLOR_KEY_MESSAGE ( `Attempting to promote package ${ packageMetadata . package_name } before installation` ) ) ; let promoteUnlockedPackageImpl = new PromoteUnlockedPackageImpl ( sourceDirectory , packageMetadata . package_version_id , this . props . devhubUserName ) ; await promoteUnlockedPackageImpl . exec ( ) ; } } } private displayRetryHeader ( isRetryOnFailure , count ) { if ( isRetryOnFailure && count > 1 ) { SFPLogger . log ( `-------------------------------------------------------------------------------${ EOL }` , LoggerLevel . INFO , this . props . packageLogger ) ; SFPLogger . log ( `Retrying On Failure Attempt: ${ count }` , LoggerLevel . INFO , this . props . packageLogger ) ; SFPLogger . log ( `-------------------------------------------------------------------------------${ EOL }` , LoggerLevel . INFO , this . props . packageLogger ) ; } } private displayHeader ( packageMetadata , pkgDescriptor , pkg ) { let alwaysDeployMessage ; if ( this . props . skipIfPackageInstalled ) { if ( pkgDescriptor . alwaysDeploy ) alwaysDeployMessage = `Always Deploy: ${ COLOR_KEY_MESSAGE ( `True` ) }` ; else alwaysDeployMessage = `Always Deploy: ${ COLOR_KEY_MESSAGE ( `False` ) }` ; } else alwaysDeployMessage = undefined ; SFPLogger . log ( COLOR_HEADER ( `-------------------------Installing Package------------------------------------` ) , LoggerLevel . INFO , this . props . packageLogger ) ; SFPLogger . log ( COLOR_HEADER ( `Name: ${ COLOR_KEY_MESSAGE ( pkg ) }` ) , LoggerLevel . INFO , this . props . packageLogger ) ; SFPLogger . log ( `Type: ${ COLOR_KEY_MESSAGE ( packageMetadata . package_type ) }` , LoggerLevel . INFO , this . props . packageLogger ) ; SFPLogger . log ( `Version Number: ${ COLOR_KEY_MESSAGE ( packageMetadata . package_version_number ) }` , LoggerLevel . INFO , this . props . packageLogger ) ; if ( pkgDescriptor . aliasfy ) SFPLogger . log ( `Aliasified Package: ${ COLOR_KEY_MESSAGE ( `True` ) }` , LoggerLevel . INFO , this . props . packageLogger ) ; SFPLogger . log ( `Contains Apex Classes/Triggers: ${ COLOR_KEY_MESSAGE ( packageMetadata . isApexFound ) }` , LoggerLevel . INFO , this . props . packageLogger ) ; if ( packageMetadata . package_type == "source" || packageMetadata . package_type == "unlocked" ) { if ( ! pkgDescriptor . aliasfy ) SFPLogger . log ( `Metadata Count: ${ COLOR_KEY_MESSAGE ( packageMetadata . metadataCount ) }` , LoggerLevel . INFO , this . props . packageLogger ) ; } if ( alwaysDeployMessage ) SFPLogger . log ( alwaysDeployMessage , LoggerLevel . INFO , this . props . packageLogger ) ; SFPLogger . log ( COLOR_HEADER ( `-------------------------------------------------------------------------------` ) , LoggerLevel . INFO , this . props . packageLogger ) ; } private printArtifactVersionsWhenSkipped ( queue , packagesToPackageInfo , isBaselinOrgModeActivated ) { this . printOpenLoggingGroup ( `Full Deployment Breakdown` ) ; let maxTable = new Table ( { head : [ "Package" , "Incoming Version" , isBaselinOrgModeActivated ? "Version in baseline org" : "Version in org" , "To be installed?" ] , } ) ; queue . forEach ( ( pkg ) => { maxTable . push ( [ pkg . package , packagesToPackageInfo [ pkg . package ] . packageMetadata . package_version_number , packagesToPackageInfo [ pkg . package ] . versionInstalledInOrg ? packagesToPackageInfo [ pkg . package ] . versionInstalledInOrg : "N/A" , packagesToPackageInfo [ pkg . package ] . isPackageInstalled ? "No" : "Yes" ] ) ; } ) ; console . log ( maxTable . toString ( ) ) ; this . printClosingLoggingGroup ( ) ; this . printOpenLoggingGroup ( `Packages to be deployed` ) ; let minTable = new Table ( { head : [ "Package" , "Incoming Version" , isBaselinOrgModeActivated ? "Version in baseline org" : "Version in org" ] , } ) ; queue . forEach ( ( pkg ) => { if ( ! packagesToPackageInfo [ pkg . package ] . isPackageInstalled ) minTable . push ( [ pkg . package , packagesToPackageInfo [ pkg . package ] . packageMetadata . package_version_number , packagesToPackageInfo [ pkg . package ] . versionInstalledInOrg ? packagesToPackageInfo [ pkg . package ] . versionInstalledInOrg : "N/A" ] ) ; } ) ; console . log ( minTable . toString ( ) ) ; this . printClosingLoggingGroup ( ) ; } private printArtifactVersions ( queue , packagesToPackageInfo ) { this . printOpenLoggingGroup ( `Packages to be deployed` ) ; let table = new Table ( { head : [ "Package" , "Version to be installed" ] , } ) ; queue . forEach ( ( pkg ) => { table . push ( [ pkg . package , packagesToPackageInfo [ pkg . package ] . packageMetadata . package_version_number ] ) ; } ) ; SFPLogger . log ( table . toString ( ) , LoggerLevel . INFO , this . props . packageLogger ) ; this . printClosingLoggingGroup ( ) ; } private async filterByPackagesInstalledInTheOrg ( packageManifest , queue , packagesToPackageInfo , targetUsername ) { const clonedQueue = [ ] ; queue . forEach ( val => clonedQueue . push ( Object . assign ( { } , val ) ) ) ; for ( var i = queue . length - 1 ; i >= 0 ; i -- ) { let packageInfo = packagesToPackageInfo [ clonedQueue [ i ] . package ] ; let packageMetadata = packageInfo . packageMetadata ; let pkgDescriptor = ProjectConfig . getPackageDescriptorFromConfig ( clonedQueue [ i ] . package , packageManifest ) ; let packageInstalledInTheOrg = await ArtifactInstallationStatusChecker . checkWhetherPackageIsIntalledInOrg ( this . props . packageLogger , targetUsername , packageMetadata ) ; if ( packageInstalledInTheOrg . versionNumber ) packageInfo . versionInstalledInOrg = packageInstalledInTheOrg . versionNumber ; if ( packageInstalledInTheOrg . isInstalled ) { if ( ! pkgDescriptor . alwaysDeploy ) { packageInfo . isPackageInstalled = true ; clonedQueue . splice ( i , 1 ) ; } } } InstalledAritfactsFetcher . resetFetchedArtifacts ( ) ; return clonedQueue ; } private printOpenLoggingGroup ( message , pkg ? ) { if ( this . props . logsGroupSymbol ?. [ 0 ] ) SFPLogger . log ( `${ this . props . logsGroupSymbol [ 0 ] } ${ message }   ${ ( pkg ? pkg : "" ) }` , LoggerLevel . INFO , this . props . packageLogger ) ; } private printClosingLoggingGroup ( ) { if ( this . props . logsGroupSymbol ?. [ 1 ] ) SFPLogger . log ( this . props . logsGroupSymbol [ 1 ] , LoggerLevel . INFO , this . props . packageLogger ) ; } param artifacts private getPackagesToPackageInfo ( artifacts ) { let packagesToPackageInfo = { } ; for ( let artifact of artifacts ) { let packageMetadata = JSON . parse ( fs . readFileSync ( artifact . packageMetadataFilePath , "utf8" ) ) ; if ( packagesToPackageInfo [ packageMetadata . package_name ] ) { let previousVersionNumber = this . convertBuildNumDotDelimToHyphen ( packagesToPackageInfo [ packageMetadata . package_name ] . packageMetadata . package_version_number ) ; let currentVersionNumber = this . convertBuildNumDotDelimToHyphen ( packageMetadata . package_version_number ) ; if ( semver . gt ( currentVersionNumber , previousVersionNumber ) ) { packagesToPackageInfo [ packageMetadata . package_name ] = { sourceDirectory : artifact . sourceDirectoryPath , packageMetadata : packageMetadata , } ; } } else { packagesToPackageInfo [ packageMetadata . package_name ] = { sourceDirectory : artifact . sourceDirectoryPath , packageMetadata : packageMetadata , } ; } } return packagesToPackageInfo ; } param version private convertBuildNumDotDelimToHyphen ( version ) { let convertedVersion = version ; let indexOfBuildNumDelimiter = this . getIndexOfBuildNumDelimeter ( version ) ; if ( version [ indexOfBuildNumDelimiter ] === "." ) { convertedVersion = version . substring ( 0 , indexOfBuildNumDelimiter ) + "-" + version . substring ( indexOfBuildNumDelimiter + 1 ) ; } return convertedVersion ; } param version private getIndexOfBuildNumDelimeter ( version ) { let numOfDelimetersTraversed = 0 ; for ( let i = 0 ; i < version . length ; i ++ ) { if ( ! Number . isInteger ( parseInt ( version [ i ] , 10 ) ) ) { numOfDelimetersTraversed ++ ; } if ( numOfDelimetersTraversed === 3 ) { return i ; } } return null ; } /**
     * Decider for which package installation type to run
     */ private async installPackage ( packageType , sfdx_package , targetUsername , sourceDirectoryPath , packageMetadata , skipTesting , waitTime , pkgDescriptor , skipIfPackageInstalled ) { let packageInstallationResult ; if ( this . props . deploymentMode == DeploymentMode . NORMAL ) { if ( packageType === "unlocked" ) { packageInstallationResult = await this . installUnlockedPackage ( targetUsername , packageMetadata , skipIfPackageInstalled , waitTime , sourceDirectoryPath ) ; } else if ( packageType === "source" ) { let options = { optimizeDeployment : this . isOptimizedDeploymentForSourcePackage ( pkgDescriptor ) , skipTesting : skipTesting , waitTime : waitTime } ; packageInstallationResult = await this . installSourcePackage ( sfdx_package , targetUsername , sourceDirectoryPath , packageMetadata , options , skipIfPackageInstalled ) ; } else if ( packageType === "data" ) { packageInstallationResult = await this . installDataPackage ( sfdx_package , targetUsername , sourceDirectoryPath , skipIfPackageInstalled , packageMetadata ) ; } else { throw new Error ( `Unhandled package type ${ packageType }` ) ; } } else if ( this . props . deploymentMode === DeploymentMode . SOURCEPACKAGES || this . props . deploymentMode === DeploymentMode . SOURCEPACKAGES_PUSH ) { if ( packageType === "source" || packageType === "unlocked" ) { let options = { optimizeDeployment : false , skipTesting : true , waitTime : waitTime } ; packageInstallationResult = await this . installSourcePackage ( sfdx_package , targetUsername , sourceDirectoryPath , packageMetadata , options , skipIfPackageInstalled ) ; } else if ( packageType === "data" ) { packageInstallationResult = await this . installDataPackage ( sfdx_package , targetUsername , sourceDirectoryPath , skipIfPackageInstalled , packageMetadata ) ; } else { throw new Error ( `Unhandled package type ${ packageType }` ) ; } } return packageInstallationResult ; } private installUnlockedPackage ( targetUsername , packageMetadata , skip_if_package_installed , waitTime , sourceDirectoryPath ) { let options = { installationkey : null , apexcompile : "package" , securitytype : "AdminsOnly" , upgradetype : "Mixed" , waitTime : waitTime } ; let installUnlockedPackageImpl = new InstallUnlockedPackageImpl ( packageMetadata . package_name , targetUsername , options , skip_if_package_installed , packageMetadata , sourceDirectoryPath , this . props . packageLogger , this . props . isDryRun ) ; return installUnlockedPackageImpl . exec ( ) ; } private installSourcePackage ( sfdx_package , targetUsername , sourceDirectoryPath , packageMetadata , options , skip_if_package_installed ) { let installSourcePackageImpl = new InstallSourcePackageImpl ( sfdx_package , targetUsername , sourceDirectoryPath , options , skip_if_package_installed , packageMetadata , this . props . packageLogger , this . props . currentStage == "prepare" ? path . join ( sourceDirectoryPath , "forceignores" , ".prepareignore" ) : null , this . props . deploymentMode === DeploymentMode . SOURCEPACKAGES_PUSH ? DeploymentType . SOURCE_PUSH : DeploymentType . MDAPI_DEPLOY , this . props . isDryRun ) ; return installSourcePackageImpl . exec ( ) ; } private installDataPackage ( sfdx_package , targetUsername , sourceDirectoryPath , skip_if_package_installed , packageMetadata ) { let installDataPackageImpl = new InstallDataPackageImpl ( sfdx_package , targetUsername , sourceDirectoryPath , packageMetadata , skip_if_package_installed , this . props . packageLogger , LoggerLevel . INFO , this . props . isDryRun ) ; return installDataPackageImpl . exec ( ) ; } private async triggerApexTests ( sfdx_package , targetUsername , skipCoverageValidation , coverageThreshold ) { let sfPackage = await SFPPackage . buildPackageFromProjectConfig ( this . props . packageLogger , null , sfdx_package , null ) ; let testOptions = new RunAllTestsInPackageOptions ( sfPackage , 60 , ".testresults" ) ; let testCoverageOptions = { isIndividualClassCoverageToBeValidated : false , isPackageCoverageToBeValidated : ! skipCoverageValidation , coverageThreshold : coverageThreshold || 75 , } ; let triggerApexTests = new TriggerApexTests ( targetUsername , testOptions , testCoverageOptions , null , this . props . packageLogger ) ; return triggerApexTests . exec ( ) ; } param packageDescriptor private isSkipDeployment ( packageDescriptor , targetUsername ) { let skipDeployOnOrgs = packageDescriptor . skipDeployOnOrgs ; if ( skipDeployOnOrgs ) { if ( ! ( skipDeployOnOrgs instanceof Array ) ) throw new Error ( `Property 'skipDeployOnOrgs' must be of type Array` ) ; else return skipDeployOnOrgs . includes ( targetUsername ) ; } else return false ; } private isOptimizedDeploymentForSourcePackage ( pkgDescriptor ) { if ( pkgDescriptor [ "isOptimizedDeployment" ] == null ) return true ; else return pkgDescriptor [ "isOptimizedDeployment" ] ; } /**
     * Returns the packages in the project config that have an artifact
     */ private getPackagesToDeploy ( packageManifest , packagesToPackageInfo ) { let packagesToDeploy ; let packages = packageManifest [ "packageDirectories" ] ; packagesToDeploy = packages . filter ( ( pkg ) => { return packagesToPackageInfo [ pkg . package ] ; } ) ; packagesToDeploy = packagesToDeploy . filter ( ( pkg ) => ! this . isSkipDeployment ( pkg , this . props . targetUsername ) ) ; packagesToDeploy = packagesToDeploy . filter ( ( pkg ) => { if ( pkg . ignoreOnStage ?. find ( ( stage ) => { stage = stage . toLowerCase ( ) ; return stage === this . props . currentStage ; } ) ) return false ; else return true ; } ) ; packagesToDeploy = packagesToDeploy . filter ( ( pkg ) => { return ! ( ( this . props . currentStage === "prepare" || this . props . currentStage === "validate" ) && pkg . aliasfy && pkg . type !== "data" ) ; } ) ; if ( packagesToDeploy == null || packagesToDeploy . length === 0 ) throw new Error ( `No artifacts from project config to be deployed` ) ; else return packagesToDeploy ; } } interface PackageInfo { sourceDirectory : string ; packageMetadata : PackageMetadata ; versionInstalledInOrg ? : string ; isPackageInstalled ? : boolean ; } export interface DeploymentResult { scheduled : number ; deployed : PackageInfo [ ] ; failed : PackageInfo [ ] ; testFailure : PackageInfo ; error : any ; }