'use strict' ; import * as vscode from 'vscode' ; import * as fs from 'fs-extra' ; import * as ejs from 'ejs' ; import * as path from 'path' ; import * as os from 'os' ; import { UserInputUtil , IBlockchainQuickPickItem , LanguageQuickPickItem } from './UserInputUtil' ; import { FabricGatewayConnectionManager } from '../fabric/FabricGatewayConnectionManager' ; import { VSCodeBlockchainOutputAdapter } from '../logging/VSCodeBlockchainOutputAdapter' ; import { Reporter } from '../util/Reporter' ; import { CommandUtil } from '../util/CommandUtil' ; import { InstantiatedTreeItem } from '../explorer/model/InstantiatedTreeItem' ; import { MetadataUtil } from '../util/MetadataUtil' ; import { ExtensionCommands } from '../../ExtensionCommands' ; import { FabricWalletRegistryEntry , IFabricGatewayConnection , LogType , FabricGatewayRegistryEntry } from 'ibm-blockchain-platform-common' ; import { ContractTreeItem } from '../explorer/model/ContractTreeItem' ; import { FabricGatewayHelper } from '../fabric/FabricGatewayHelper' ; import { FABRIC_NETWORK_NODE_VERSION , FABRIC_NETWORK_JAVA_VERSION } from '../util/ExtensionUtil' ; export async function testSmartContract ( allContracts , chaincode ? ) { let chaincodeLabel ; let chosenChaincode ; let channelName ; let chaincodeName ; let chaincodeVersion ; const outputAdapter = VSCodeBlockchainOutputAdapter . instance ( ) ; outputAdapter . log ( LogType . INFO , undefined , `testSmartContractCommand` ) ; if ( ! chaincode ) { if ( ! FabricGatewayConnectionManager . instance ( ) . getConnection ( ) ) { await vscode . commands . executeCommand ( ExtensionCommands . CONNECT_TO_GATEWAY ) ; if ( ! FabricGatewayConnectionManager . instance ( ) . getConnection ( ) ) { return ; } } chosenChaincode = await UserInputUtil . showClientInstantiatedSmartContractsQuickPick ( 'Please choose instantiated smart contract to test' ) ; if ( ! chosenChaincode ) { return ; } chaincodeLabel = chosenChaincode . label ; chaincodeName = chosenChaincode . data . name ; chaincodeVersion = chosenChaincode . data . version ; channelName = chosenChaincode . data . channel ; } else { if ( chaincode instanceof ContractTreeItem ) { chaincodeLabel = chaincode . instantiatedChaincode . label ; chaincodeName = chaincode . instantiatedChaincode . name ; channelName = chaincode . instantiatedChaincode . channels [ 0 ] . label ; chaincodeVersion = chaincode . instantiatedChaincode . version ; } else { chaincodeLabel = chaincode . label ; chaincodeName = chaincode . name ; channelName = chaincode . channels [ 0 ] . label ; chaincodeVersion = chaincode . version ; } } const connection = FabricGatewayConnectionManager . instance ( ) . getConnection ( ) ; let transactions = await MetadataUtil . getTransactions ( connection , chaincodeName , channelName , true ) ; if ( ! transactions || transactions . size === 0 ) { outputAdapter . log ( LogType . ERROR , `Populated metadata required for generating smart contract tests, see previous error` ) ; return ; } if ( ( chaincode && chaincode instanceof ContractTreeItem ) || ( ! allContracts && ! chaincode && transactions . size > 1 ) ) { let chosenContract ; if ( chaincode && chaincode instanceof ContractTreeItem ) { chosenContract = chaincode . name ; } else { chosenContract = await UserInputUtil . showContractQuickPick ( 'Choose a contract to generate tests for' , Array . from ( transactions . keys ( ) ) ) ; if ( ! chosenContract ) { return ; } } const tempTransactions = transactions ; transactions = new Map < string , any [ ] > ( ) ; transactions . set ( chosenContract , tempTransactions . get ( chosenContract ) ) ; } let chosenPackageFolder ; try { chosenPackageFolder = await UserInputUtil . chooseWorkspace ( false ) ; if ( ! chosenPackageFolder ) { return ; } } catch ( err ) { outputAdapter . log ( LogType . ERROR , err . message , err . toString ( ) ) ; return ; } const contractLang = await UserInputUtil . getLanguage ( chosenPackageFolder ) ; let testLang ; let functionalTestsDirectory ; let localSmartContractDirectory ; let testFileSuffix ; let workspaceProjectName ; const languagesToSuffixes = { JavaScript : 'js' , TypeScript : 'ts' , } ; if ( contractLang === 'java' ) { languagesToSuffixes [ 'Java' ] = 'java' ; testLang = 'Java' ; functionalTestsDirectory = path . join ( chosenPackageFolder . uri . fsPath , 'src' , 'test' , 'java' , 'org' , 'example' ) ; } else if ( contractLang === 'golang' ) { languagesToSuffixes [ 'Golang' ] = 'go' ; testLang = 'Golang' ; functionalTestsDirectory = chosenPackageFolder . uri . fsPath ; } else { const packageJsonFile = path . join ( chosenPackageFolder . uri . fsPath , 'package.json' ) ; const packageJSONBuffer = await fs . readFile ( packageJsonFile ) ; const packageJSONObj = JSON . parse ( packageJSONBuffer . toString ( 'utf8' ) ) ; const replaceRegex = /@.*?\// ; workspaceProjectName = packageJSONObj . name ; workspaceProjectName = workspaceProjectName . replace ( replaceRegex , '' ) ; if ( workspaceProjectName === chaincodeName ) { functionalTestsDirectory = path . join ( chosenPackageFolder . uri . fsPath , 'functionalTests' ) ; localSmartContractDirectory = chosenPackageFolder . uri . fsPath ; const testLanguageItem = await UserInputUtil . showLanguagesQuickPick ( 'Choose preferred test language' , [ ] , Object . keys ( languagesToSuffixes ) ) ; if ( ! testLanguageItem ) { return ; } testLang = testLanguageItem . label ; } else { outputAdapter . log ( LogType . ERROR , `Smart contract project ${ chaincodeName } does not correspond to the selected open project ${ workspaceProjectName }.` ) ; return ; } } testFileSuffix = languagesToSuffixes [ testLang ] ; let isPrivateData = false ; const userPrivateData = await UserInputUtil . showQuickPickYesNo ( 'Is this a private data contract?' ) ; if ( userPrivateData === undefined ) { return ; } else if ( userPrivateData === UserInputUtil . YES ) { isPrivateData = true ; } const fabricConnectionManager = FabricGatewayConnectionManager . instance ( ) ; const fabricGatewayRegistryEntry = await fabricConnectionManager . getGatewayRegistryEntry ( ) ; const fabricWalletRegistryEntry = fabricConnectionManager . getConnectionWallet ( ) ; const connectionIdentityName = fabricConnectionManager . getConnectionIdentity ( ) ; for ( const [ contractName , transactionArray ] of transactions ) { const homedir = os . homedir ( ) ; let walletPathString ; let walletHome ; let pathWithoutHomeDir ; let connectionProfilePath ; let conProfileHome ; const separator = process . platform === 'win32' ? '\\' : '/' ; const connectionProfilePathTmp = await FabricGatewayHelper . getConnectionProfilePath ( fabricGatewayRegistryEntry ) ; if ( connectionProfilePathTmp . includes ( homedir ) ) { connectionProfilePath = 'path.join(homedir' ; pathWithoutHomeDir = connectionProfilePathTmp . slice ( homedir . length + 1 ) ; pathWithoutHomeDir . split ( separator ) . forEach ( ( item ) => { connectionProfilePath += `, '${ item }'` ; } ) ; connectionProfilePath += ')' ; conProfileHome = true ; } else { connectionProfilePath = process . platform === 'win32' ? connectionProfilePathTmp . replace ( /\\/g , '\\\\' ) : connectionProfilePathTmp ; conProfileHome = false ; } if ( fabricWalletRegistryEntry . walletPath . includes ( homedir ) ) { walletPathString = 'path.join(homedir' ; pathWithoutHomeDir = fabricWalletRegistryEntry . walletPath . slice ( homedir . length + 1 ) ; pathWithoutHomeDir . split ( separator ) . forEach ( ( item ) => { walletPathString += `, '${ item }'` ; } ) ; walletPathString += ')' ; walletHome = true ; } else { walletPathString = process . platform === 'win32' ? fabricWalletRegistryEntry . walletPath . replace ( /\\/g , '\\\\' ) : fabricWalletRegistryEntry . walletPath ; walletHome = false ; } const templateData = { contractName : contractName , chaincodeLabel : chaincodeLabel , transactions : transactionArray , connectionProfileHome : connectionProfilePath , walletPath : walletPathString , walletHome : walletHome , chaincodeName : chaincodeName , chaincodeVersion : chaincodeVersion , channelName : channelName , identityName : connectionIdentityName , conProfileHome : conProfileHome , isPrivateData : isPrivateData } ; const utilTemplateData = { connectionProfilePath : connectionProfilePath , chaincodeName : chaincodeName , channelName : channelName , walletHome : walletHome , conProfileHome : conProfileHome , isPrivateData : isPrivateData } ; const template = path . join ( __dirname , '..' , '..' , '..' , 'templates' , `${ testFileSuffix }TestSmartContractTemplate.ejs` ) ; const utilTemplate = path . join ( __dirname , '..' , '..' , '..' , 'templates' , `${ testFileSuffix }TestSmartContractUtilTemplate.ejs` ) ; let dataToWrite ; let functionDataToWrite ; try { dataToWrite = await createDataToWrite ( template , templateData ) ; functionDataToWrite = await createDataToWrite ( utilTemplate , utilTemplateData ) ; } catch ( error ) { outputAdapter . log ( LogType . ERROR , `Error creating template data: ${ error . message }` , `Error creating template data: ${ error . toString ( ) }` ) ; return ; } let testFile ; let testFunctionFile ; if ( testLang === 'Java' ) { const capsChaincodeLabelForJava = chaincodeLabel [ 0 ] . toUpperCase ( ) + chaincodeLabel . slice ( 1 ) . replace ( /\./g , '' ) . replace ( '@' , '' ) ; if ( contractName !== '' ) { const capsContractName = contractName [ 0 ] . toUpperCase ( ) + contractName . slice ( 1 ) ; testFile = path . join ( functionalTestsDirectory , `Fv${ capsContractName }${ capsChaincodeLabelForJava }Test.${ testFileSuffix }` ) ; } else { testFile = path . join ( functionalTestsDirectory , `Fv${ capsChaincodeLabelForJava }Test.${ testFileSuffix }` ) ; } testFunctionFile = path . join ( functionalTestsDirectory , `${ testLang }SmartContractUtil.${ testFileSuffix }` ) ; } else if ( testLang === 'Golang' ) { if ( contractName !== '' ) { testFile = path . join ( functionalTestsDirectory , `fv-${ contractName }-${ chaincodeLabel }_test.${ testFileSuffix }` ) ; } else { testFile = path . join ( functionalTestsDirectory , `fv-${ chaincodeLabel }_test.${ testFileSuffix }` ) ; } testFunctionFile = path . join ( functionalTestsDirectory , `${ testFileSuffix }-smart-contract-util.${ testFileSuffix }` ) ; } else { if ( contractName !== '' ) { testFile = path . join ( functionalTestsDirectory , `${ contractName }-${ chaincodeLabel }.test.${ testFileSuffix }` ) ; } else { testFile = path . join ( functionalTestsDirectory , `${ chaincodeLabel }.test.${ testFileSuffix }` ) ; } testFunctionFile = path . join ( functionalTestsDirectory , `${ testFileSuffix }-smart-contract-util.${ testFileSuffix }` ) ; } const testFileExists = await fs . pathExists ( testFile ) ; let overwriteTestFile ; if ( testFileExists ) { overwriteTestFile = await UserInputUtil . showTestFileOverwriteQuickPick ( 'Test file for selected smart contract already exists in workspace, overwrite it?' ) ; if ( ! overwriteTestFile ) { outputAdapter . log ( LogType . INFO , undefined , `Preserving test file for instantiated smart contract located here: ${ testFile }` ) ; return ; } } if ( overwriteTestFile === UserInputUtil . NO ) { outputAdapter . log ( LogType . INFO , undefined , `Preserving test file for instantiated smart contract located here: ${ testFile }` ) ; return ; } else if ( overwriteTestFile === UserInputUtil . GENERATE_NEW_TEST_FILE ) { let i = 1 ; while ( await fs . pathExists ( testFile ) ) { if ( testLang === 'Java' ) { const capsChaincodeLabelForJava = chaincodeLabel [ 0 ] . toUpperCase ( ) + chaincodeLabel . slice ( 1 ) . replace ( /\./g , '' ) . replace ( '@' , '' ) ; if ( contractName !== '' ) { const capsContractName = contractName [ 0 ] . toUpperCase ( ) + contractName . slice ( 1 ) ; testFile = path . join ( functionalTestsDirectory , `Fv${ capsContractName }${ capsChaincodeLabelForJava }Copy${ i }Test.${ testFileSuffix }` ) ; } else { testFile = path . join ( functionalTestsDirectory , `Fv${ capsChaincodeLabelForJava }Copy${ i }Test.${ testFileSuffix }` ) ; } } else { if ( contractName !== '' ) { testFile = path . join ( functionalTestsDirectory , `${ contractName }-${ chaincodeLabel }-copy${ i }.test.${ testFileSuffix }` ) ; } else { testFile = path . join ( functionalTestsDirectory , `${ chaincodeLabel }-copy${ i }.test.${ testFileSuffix }` ) ; } } i ++ ; } } try { await fs . ensureFile ( testFile ) ; } catch ( error ) { outputAdapter . log ( LogType . ERROR , `Error creating test file: ${ error . message }` , `Error creating test file: ${ error . toString ( ) }` ) ; await removeTestFile ( testFile ) ; return ; } outputAdapter . log ( LogType . INFO , undefined , `Writing to Smart Contract test file: ${ testFile }` ) ; const testFunctionFileExists = await fs . pathExists ( testFunctionFile ) ; if ( ! testFunctionFileExists || overwriteTestFile ) { try { await fs . ensureFile ( testFunctionFile ) ; } catch ( error ) { outputAdapter . log ( LogType . ERROR , `Error creating test util file: ${ error . message }` , `Error creating test util file: ${ error . toString ( ) }` ) ; await removeTestFile ( testFunctionFile ) ; return ; } const functionDocument = await vscode . workspace . openTextDocument ( testFunctionFile ) ; const editor = await vscode . window . showTextDocument ( functionDocument ) ; const functionLineCount = functionDocument . lineCount ; const editorResult = await editor . edit ( ( editBuilder ) => { editBuilder . replace ( new vscode . Range ( 0 , 0 , functionLineCount , 0 ) , functionDataToWrite ) ; } ) ; if ( ! editorResult ) { outputAdapter . log ( LogType . ERROR , `Error editing test util file: ${ testFunctionFile }` ) ; await removeTestFile ( testFunctionFile ) ; return ; } await functionDocument . save ( ) ; } const document = await vscode . workspace . openTextDocument ( testFile ) ; const showTextDocumentOptions = { preserveFocus : true , selection : new vscode . Range ( 0 , 0 , 12 , 2 ) } ; const textEditor = await vscode . window . showTextDocument ( document , showTextDocumentOptions ) ; const lineCount = document . lineCount ; const textEditorResult = await textEditor . edit ( ( editBuilder ) => { editBuilder . replace ( new vscode . Range ( 0 , 0 , lineCount , 0 ) , dataToWrite ) ; } ) ; if ( ! textEditorResult ) { outputAdapter . log ( LogType . ERROR , `Error editing test file: ${ testFile }` ) ; await removeTestFile ( testFile ) ; return ; } await document . save ( ) ; } let javaBuildFile = null ; let state ; if ( testLang === 'Java' ) { state = { tryCopy : false , doneCopy : false , needUpdate : false , tryUpdate : false , doneupdate : false , success : false , } ; try { javaBuildFile = await whichBuildFile ( chosenPackageFolder ) ; await updateBuildFile ( javaBuildFile , state ) ; } catch ( error ) { outputAdapter . log ( LogType . ERROR , `Unable to modify the build file in folder ${ chosenPackageFolder . uri . fsPath }: ${ error . message }` , `Unable to modify the build file in folder ${ chosenPackageFolder . uri . fsPath }: ${ error . toString ( ) }` ) ; } if ( state . doneCopy ) { const writeFailed = state . needUpdate && state . tryUpdate && ! state . doneupdate ; try { await mvOrRmBuildFileCopy ( chosenPackageFolder , javaBuildFile , writeFailed ) ; } catch ( error ) { outputAdapter . log ( LogType . ERROR , `Unable to cleanup tmp build file: ${ error . message }` , `Unable to cleanup tmp build file: ${ error . toString ( ) }` ) ; return ; } } if ( ! state . success ) { return ; } } else if ( testLang === 'Golang' ) { outputAdapter . log ( LogType . INFO , `Running 'go mod vendor' in ${ chosenPackageFolder . uri . fsPath }` ) ; try { await CommandUtil . sendCommandWithOutput ( 'go' , [ 'mod' , 'vendor' ] , chosenPackageFolder . uri . fsPath ) ; } catch ( error ) { outputAdapter . log ( LogType . ERROR , `Error running 'go mod vendor' in ${ chosenPackageFolder . uri . fsPath }: ${ error . message }` , `Error running 'go mod vendor' in ${ chosenPackageFolder . uri . fsPath }: ${ error . toString ( ) }` ) ; return ; } } else { try { await installNodeModules ( localSmartContractDirectory , testLang ) ; } catch ( error ) { outputAdapter . log ( LogType . ERROR , `Error installing node modules in smart contract project: ${ error . message }` , `Error installing node modules in smart contract project: ${ error . toString ( ) }` ) ; return ; } } if ( testLang === 'TypeScript' ) { const runnerArgs = vscode . workspace . getConfiguration ( ) . get ( 'javascript-test-runner.additionalArgs' ) ; if ( ! runnerArgs || ! runnerArgs . includes ( '-r ts-node/register' ) ) { try { await vscode . workspace . getConfiguration ( ) . update ( 'javascript-test-runner.additionalArgs' , '-r ts-node/register' , vscode . ConfigurationTarget . Global ) ; } catch ( err ) { const message = 'Unable to update javascript-test-runner.additionalArgs for running TypeScript tests. Add "javascript-test-runner.additionalArgs": "-r ts-node/register", to your VSCode settings (JSON)' ; outputAdapter . log ( LogType . ERROR , message ) ; } } const tsConfigPath = path . join ( localSmartContractDirectory , 'tsconfig.json' ) ; const tsConfigContents = { compilerOptions : { outDir : 'dist' , target : 'es2017' , moduleResolution : 'node' , module : 'commonjs' , declaration : true , sourceMap : true } , include : [ './functionalTests/**/*' ] , exclude : [ 'node_modules' ] } ; try { const tsConfigExists = await fs . pathExists ( tsConfigPath ) ; if ( ! tsConfigExists ) { await fs . writeJson ( tsConfigPath , tsConfigContents , { spaces : '\t' } ) ; } else { outputAdapter . log ( LogType . WARNING , `Unable to create tsconfig.json file as it already exists` ) ; } } catch ( error ) { outputAdapter . log ( LogType . ERROR , `Unable to create tsconfig.json file: ${ error . message }` , `Unable to create tsconfig.json file: ${ error . toString ( ) }` ) ; return ; } } let successMessage = 'Successfully generated tests' ; if ( testLang === 'Java' && ! state . tryCopy && javaBuildFile !== null ) { successMessage += ` - didn't overwrite ${ javaBuildFile }, see README for dependency details.` ; } outputAdapter . log ( LogType . SUCCESS , successMessage ) ; Reporter . instance ( ) . sendTelemetryEvent ( 'testSmartContractCommand' , { testSmartContractLanguage : testLang } ) ; } async function createDataToWrite ( template , templateData ) { const ejsOptions = { async : true , } ; return new Promise ( ( resolve , reject ) : any => { ejs . renderFile ( template , templateData , ejsOptions , ( error , data ) => { if ( error ) { reject ( error ) ; } else { resolve ( data ) ; } } ) ; } ) ; } async function removeTestFile ( fileToRemove ) { const outputAdapter = VSCodeBlockchainOutputAdapter . instance ( ) ; try { await fs . remove ( fileToRemove ) ; } catch ( error ) { if ( ! error . message . includes ( 'ENOENT: no such file or directory' ) ) { outputAdapter . log ( LogType . ERROR , `Error removing test file: ${ error . message }` , `Error removing test file: ${ error . toString ( ) }` ) ; return ; } } } async function installNodeModules ( dir , language ) { const outputAdapter = VSCodeBlockchainOutputAdapter . instance ( ) ; let npmInstallOut ; if ( language === 'TypeScript' ) { outputAdapter . log ( LogType . INFO , `Installing package dependencies including: fabric-network@${ FABRIC_NETWORK_NODE_VERSION }, @types/mocha, ts-node, typescript` ) ; npmInstallOut = await CommandUtil . sendCommandWithProgress ( `npm install && npm install --save-dev fabric-network@${ FABRIC_NETWORK_NODE_VERSION } @types/mocha ts-node typescript` , dir , 'Installing npm and package dependencies in smart contract project' ) ; } else { outputAdapter . log ( LogType . INFO , `Installing package dependencies including: fabric-network@${ FABRIC_NETWORK_NODE_VERSION }` ) ; npmInstallOut = await CommandUtil . sendCommandWithProgress ( `npm install && npm install --save-dev fabric-network@${ FABRIC_NETWORK_NODE_VERSION }` , dir , 'Installing npm and package dependencies in smart contract project' ) ; } outputAdapter . log ( LogType . INFO , undefined , npmInstallOut ) ; } async function whichBuildFile ( packageFolder ) { let buildFile = null ; const gradleFile = path . join ( packageFolder . uri . fsPath , 'build.gradle' ) ; const mavenFile = path . join ( packageFolder . uri . fsPath , 'pom.xml' ) ; const gradleFileExists = await fs . pathExists ( gradleFile ) ; const mavenFileExists = await fs . pathExists ( mavenFile ) ; if ( gradleFileExists ) { buildFile = gradleFile ; } else if ( mavenFileExists ) { buildFile = mavenFile ; } else { throw new Error ( 'Could not locate a build file.' ) ; } return buildFile ; } async function updateBuildFile ( buildFile , state ) { const isGradle = buildFile . endsWith ( 'build.gradle' ) ; const reallyDoIt = await UserInputUtil . showConfirmationWarningMessage ( `This task might overwrite ${ buildFile }. Do you wish to continue?` ) ; if ( ! reallyDoIt ) { state . success = true ; return ; } state . tryCopy = true ; await fs . copy ( buildFile , `${ buildFile }.tmp` ) ; state . doneCopy = true ; const additionalRequirements = { dependencies : { 'fabric-gateway-java' : { groupId : 'org.hyperledger.fabric' , artifactId : 'fabric-gateway-java' , version : FABRIC_NETWORK_JAVA_VERSION , present : false , } , 'assertj-core' : { groupId : 'org.assertj' , artifactId : 'assertj-core' , version : '3.14.0' , present : false , } , 'jackson-dataformat' : { groupId : 'com.fasterxml.jackson.dataformat' , artifactId : 'jackson-dataformat-yaml' , version : '2.10.0' , present : false , } , 'jackson-databind' : { groupId : 'com.fasterxml.jackson.core' , artifactId : 'jackson-databind' , version : '2.10.0' , present : false , } , } } ; const originalContents = await fs . readFile ( buildFile , 'utf8' ) ; state . needUpdate = checkContent ( originalContents , additionalRequirements ) ; if ( state . needUpdate ) { const currentDep = getProperty ( originalContents , 'dependencies' , isGradle ) ; const replaceDep = getReplacement ( currentDep , additionalRequirements , 'dependencies' , isGradle ) ; const modifiedContents = originalContents . replace ( currentDep , replaceDep ) ; state . tryUpdate = true ; await fs . writeFile ( buildFile , modifiedContents ) ; state . doneupdate = true ; } state . success = true ; } function checkContent ( content , requirements ) { let needUpdate = false ; for ( const obj in requirements ) { for ( const req in requirements [ obj ] ) { const searchItem = requirements [ obj ] [ req ] . artifactId ; if ( content . indexOf ( searchItem ) > - 1 ) { requirements [ obj ] [ req ] . present = true ; } else { needUpdate = true ; } } } return needUpdate ; } function getProperty ( data , property , isGradle ) { let start = - 1 ; let end = - 1 ; if ( isGradle ) { start = data . indexOf ( property ) ; if ( start !== - 1 ) { let count = 0 ; for ( let i = start + data . slice ( start ) . indexOf ( '{' ) ; i < data . length ; i ++ ) { count += data [ i ] === '{' ? 1 : data [ i ] === '}' ? - 1 : 0 ; if ( count === 0 && i > start ) { end = i + 1 ; break ; } } } } else { start = data . indexOf ( `<${ property }>` ) ; end = data . indexOf ( `</${ property }>` ) + `</${ property }>` . length ; } if ( start === - 1 || end === - 1 ) { throw new Error ( `Could not find property '${ property }'.` ) ; } return data . slice ( start , end ) ; } function getReplacement ( data , requirements , type , isGradle ) { for ( const obj in requirements [ type ] ) { if ( ! requirements [ type ] [ obj ] . present ) { let replacement ; if ( isGradle ) { replacement = `\ttestImplementation '${ requirements [ type ] [ obj ] . groupId }:${ requirements [ type ] [ obj ] . artifactId }:${ requirements [ type ] [ obj ] . version }'\n}` ; data = data . replace ( /\n*}$/ , '\n}' ) . replace ( /}$/ , replacement ) ; } else { replacement = `\n\t\t<dependency>\n\t\t\t<groupId>${ requirements [ type ] [ obj ] . groupId }</groupId>\n\t\t\t<artifactId>${ requirements [ type ] [ obj ] . artifactId }</artifactId>\n\t\t\t<version>${ requirements [ type ] [ obj ] . version }</version>\n\t\t</dependency>\n\t</dependencies>` ; data = data . replace ( /\n*\s*<\/dependencies>/ , replacement ) ; } } } return data ; } async function mvOrRmBuildFileCopy ( packageFolder , buildFile , writeFailed ) { const buildFileTmp = `${ buildFile }.tmp` ; const buildFileCopyExists = await fs . pathExists ( buildFileTmp ) ; const buildFileCopy = buildFileCopyExists ? buildFileTmp : null ; if ( buildFileCopy === null ) { throw new Error ( `Could not locate tmp build file in folder ${ packageFolder . uri . fsPath }` ) ; } else if ( ! writeFailed ) { await fs . remove ( buildFileCopy ) ; } else { await fs . move ( buildFileCopy , buildFileCopy . replace ( /.tmp$/ , '' ) , { overwrite : true } ) ; } }