diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index d5a5d8e0..3130ccf8 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -14,14 +14,22 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + node: [12] steps: - uses: actions/checkout@v2 - - name: Use Node.js + - name: Use Node.js ${{ matrix.node }} uses: actions/setup-node@v1 with: - node-version: '12.x' + node-version: ${{ matrix.node }} + - name: Set up Python 3.x + uses: actions/setup-python@v2 + with: + python-version: '3.x' - run: npm install - run: npm link - run: npm run integration-test diff --git a/docs/concepts/Deploy-Command.md b/docs/concepts/Deploy-Command.md index 24a420ba..1f401760 100644 --- a/docs/concepts/Deploy-Command.md +++ b/docs/concepts/Deploy-Command.md @@ -38,15 +38,50 @@ In the deploy process of skillCode, CLI helps skill developers build all of thei ### Code Builder To support the building of multiple programming languages, CLI's philosophy is to provide a **built-in or customized** build-flows (*i.e. {programmingLanguage}-{builderTool}*) to fullfill normal needs as well as special requests. This is called `CodeBuilder` in CLI. -* Most use cases will be covered by using the **built-in** build-flows. When building skillCode with this flow, CLI will infer the builderTool (based on the type of builder's config file) from the codebase, and further decide which build-flow to execute. Build-flows are represented by cross-OS executable scripts. Current built-in build-flows include: - * nodejs-npm [(scripts)](https://github.com/alexa/ask-cli/tree/master/lib/builtins/build-flows/nodejs-npm) - * python-pip [(scripts)](https://github.com/alexa/ask-cli/tree/master/lib/builtins/build-flows/python-pip) - * java-mvn [(scripts)](https://github.com/alexa/ask-cli/tree/master/lib/builtins/build-flows/java-mvn) +* Most use cases will be covered by using the **built-in** build-flows. When building skillCode with this flow, CLI will infer the builderTool (based on the type of builder's config file) from the codebase, and further decide which build-flow to execute. * For developers who have further desire to **customize** the build-flow by themselves, CLI also supports the `custom` type of build-flow. If you provide the hook script in the following location, the script will be executed instead of using the built-in build-flows, and it is codebase-agnostic: * Non-Windows path: `{projectRoot}/hooks/build.sh` file * Windows path: `{projectRoot}\hooks\build.ps1` file * Each build flow (either built-in or customized) is executed with two parameters: the path to the build file and if it is verbose and need debug information. +#### Examples of customized build-flows + +`{projectRoot}/hooks/build.sh` +``` +#!/bin/bash + +readonly OUT_FILE=$1 +readonly DO_DEBUG=$2 + +echo $OUT_FILE +echo $DO_DEBUG + +# custom logic +``` + +`{projectRoot}\hooks\build.ps1` +``` +#requires -version 3 + +#----------------[ Parameters ]---------------------------------------------------- +param( + [Parameter(Mandatory = $false, + ValueFromPipelineByPropertyName = $true, + HelpMessage = "Name for the AWS Lambda deployable archive")] + [ValidateNotNullOrEmpty()] + [string] + $script:OutFile = "upload.zip", + + # Provide additional debug information during script run + [Parameter(Mandatory = $false, + ValueFromPipelineByPropertyName = $true, + HelpMessage = "Enable verbose output")] + [bool] + $script:Verbose = $false +) + +# custom logic +``` ## Skill Infrastructure To provision the backend services which are used to execute customized logics for Alexa skills, CLI introduces the `skillInfrastructure` concept to incorporate different deploy mechanisms into one platform. Each deployment flow is presented as a type of deployer, which is the value set in `skillInfrastructure.type`. Another two fields, `skillInfrastructure.userConfig` is designed to configure the deployment, and `skillInfrastructure.deployState` is used to facilitate CD and is not supposed to be modified manually. diff --git a/lib/builtins/build-flows/abstract-build-flow.js b/lib/builtins/build-flows/abstract-build-flow.js new file mode 100644 index 00000000..37804324 --- /dev/null +++ b/lib/builtins/build-flows/abstract-build-flow.js @@ -0,0 +1,93 @@ +const AdmZip = require('adm-zip'); +const childProcess = require('child_process'); +const path = require('path'); + +const Messenger = require('@src/view/messenger'); + +class AbstractBuildFlow { + /** + * Constructor + * @param {Object} options + * @param {String} options.cwd working directory for build + * @param {String} options.src source directory + * @param {String} options.buildFile full path for zip file to generate + * @param {Boolean} options.doDebug debug flag + */ + constructor({ cwd, src, buildFile, doDebug }) { + this.cwd = cwd; + this.src = src; + this.buildFile = buildFile; + this.stdio = doDebug ? 'inherit' : 'pipe'; + this.doDebug = !!doDebug; + this.isWindows = process.platform === 'win32'; + this.defaultZipFileDate = new Date(1990, 1, 1); + } + + /** + * Creates build zip file + * @param {Object} options + * @param {Object} options.filter filter to apply to exclude files from zip + * @param {Function} callback + */ + createZip(options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + const filter = options.filter || (() => false); + + this.debug(`Zipping source files and dependencies to ${this.buildFile}.`); + const zip = new AdmZip(); + const zipFileName = path.basename(this.buildFile); + + // adding files + zip.addLocalFolder(this.cwd, '', (entry) => entry !== zipFileName && !filter(entry)); + // setting create timestamp to the same value to allow consistent hash + zip.getEntries().forEach(e => { e.header.time = this.defaultZipFileDate; }); + + zip.writeZip(this.buildFile, callback); + } + + /** + * Modifies build zip file + * @param {Object} options + * @param {Object} options.entryProcessor function to apply to each zip file entry + * @param {Function} callback + */ + modifyZip(options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + const zip = new AdmZip(this.buildFile); + + const entryProcessor = options.entryProcessor || (() => {}); + + zip.getEntries().forEach(e => { + // setting create timestamp to the same value to allow consistent hash + e.header.time = this.defaultZipFileDate; + entryProcessor(e); + }); + zip.writeZip(this.buildFile, callback); + } + + /** + * Executes shell command + * @param {String} cmd command + */ + execCommand(cmd) { + childProcess.execSync(cmd, { cwd: this.cwd, stdio: this.stdio }); + } + + /** + * Outputs debug message + * @param {String} message message + */ + debug(message) { + if (this.doDebug) { + Messenger.getInstance().debug(message); + } + } +} + +module.exports = AbstractBuildFlow; diff --git a/lib/builtins/build-flows/custom.js b/lib/builtins/build-flows/custom.js new file mode 100644 index 00000000..0d1baf13 --- /dev/null +++ b/lib/builtins/build-flows/custom.js @@ -0,0 +1,50 @@ +const fs = require('fs-extra'); +const path = require('path'); + +const AbstractBuildFlow = require('./abstract-build-flow'); + +class CustomBuildFlow extends AbstractBuildFlow { + /** + * If this file exists than the build flow can handle build + */ + static get manifest() { return process.platform === 'win32' ? 'build.ps1' : 'build.sh'; } + + static get _customScriptPath() { return path.join(process.cwd(), 'hooks', CustomBuildFlow.manifest); } + + /** + * Returns true if the build flow can handle the build + */ + static canHandle() { + return fs.existsSync(CustomBuildFlow._customScriptPath); + } + + /** + * Constructor + * @param {Object} options + * @param {String} options.cwd working directory for build + * @param {String} options.src source directory + * @param {String} options.buildFile full path for zip file to generate + * @param {Boolean} options.doDebug debug flag + */ + constructor({ cwd, src, buildFile, doDebug }) { + super({ cwd, src, buildFile, doDebug }); + } + + /** + * Executes build + * @param {Function} callback + */ + execute(callback) { + this.debug(`Executing custom hook script ${CustomBuildFlow._customScriptPath}.`); + let command = `${CustomBuildFlow._customScriptPath} "${this.buildFile}" ${this.doDebug}`; + if (this.isWindows) { + const powerShellPrefix = 'PowerShell.exe -Command'; + const doDebug = this.doDebug ? '$True' : '$False'; + command = `${powerShellPrefix} "& {& '${CustomBuildFlow._customScriptPath}' '${this.buildFile}' ${doDebug} }"`; + } + this.execCommand(command); + callback(); + } +} + +module.exports = CustomBuildFlow; diff --git a/lib/builtins/build-flows/java-mvn.js b/lib/builtins/build-flows/java-mvn.js new file mode 100644 index 00000000..5fd3cc32 --- /dev/null +++ b/lib/builtins/build-flows/java-mvn.js @@ -0,0 +1,57 @@ +const fs = require('fs-extra'); +const path = require('path'); + +const AbstractBuildFlow = require('./abstract-build-flow'); + +class JavaMvnBuildFlow extends AbstractBuildFlow { + /** + * If this file exists than the build flow can handle build + */ + static get manifest() { return 'pom.xml'; } + + /** + * Returns true if the build flow can handle the build + */ + static canHandle({ src }) { + return fs.existsSync(path.join(src, JavaMvnBuildFlow.manifest)); + } + + /** + * Constructor + * @param {Object} options + * @param {String} options.cwd working directory for build + * @param {String} options.src source directory + * @param {String} options.buildFile full path for zip file to generate + * @param {Boolean} options.doDebug debug flag + */ + constructor({ cwd, src, buildFile, doDebug }) { + super({ cwd, src, buildFile, doDebug }); + } + + /** + * Executes build + * @param {Function} callback + */ + execute(callback) { + this.debug(`Building skill artifacts based on the ${JavaMvnBuildFlow.manifest}.`); + this.execCommand('mvn clean org.apache.maven.plugins:maven-assembly-plugin:2.6:assembly ' + + '-DdescriptorId=jar-with-dependencies package'); + const targetFolderPath = path.join(this.cwd, 'target'); + const jarFileName = fs.readdirSync(targetFolderPath).find(fileName => fileName.endsWith('jar-with-dependencies.jar')); + const jarFilePath = path.join(targetFolderPath, jarFileName); + this.debug(`Renaming the jar file ${jarFilePath} to ${this.buildFile}.`); + fs.moveSync(jarFilePath, this.buildFile, { overwrite: true }); + + this.modifyZip({ entryProcessor: this._removeCommentsFromPomProperties }, callback); + } + + _removeCommentsFromPomProperties(entry) { + // removing comment to allow consistent hashing + if (entry.entryName.includes('pom.properties')) { + const data = entry.getData().toString().replace(/^#.*\n?/mg, ''); + entry.setData(data); + } + } +} + +module.exports = JavaMvnBuildFlow; diff --git a/lib/builtins/build-flows/java-mvn/build.ps1 b/lib/builtins/build-flows/java-mvn/build.ps1 deleted file mode 100644 index 8ee1897a..00000000 --- a/lib/builtins/build-flows/java-mvn/build.ps1 +++ /dev/null @@ -1,137 +0,0 @@ -#requires -version 3 -<# - .SYNOPSIS - PowerShell script for ask-cli's Java-mvn code building flow. - .DESCRIPTION - This is the PowerShell version of the build script, for building the AWS Lambda deployable skill code that is written in Java language. This script is only run by the ask-cli whenever a 'pom.xml' file is found alongside the skill code. The dependencies are installed and packaged using 'mvn'. - .EXAMPLE - build.ps1 archive.zip - This example showcases how to run the build script, to create an AWS Lambda deployable package called 'archive.zip'. - .EXAMPLE - build.ps1 archive.zip $true - This example showcases how to run the previous example, with additional debug information. -#> -#----------------[ Parameters ]---------------------------------------------------- -param( - [Parameter(Mandatory = $false, - ValueFromPipelineByPropertyName = $true, - HelpMessage = "Name for the AWS Lambda deployable archive")] - [ValidateNotNullOrEmpty()] - [string] - $script:OutFile = "upload.zip", - - # Provide additional debug information during script run - [Parameter(Mandatory = $false, - ValueFromPipelineByPropertyName = $true, - HelpMessage = "Enable verbose output")] - [bool] - $script:Verbose = $false -) - -#----------------[ Declarations ]---------------------------------------------------- -$ErrorActionPreference = "Stop" - -#----------------[ Functions ]---------------------------------------------------- -function Show-Log() { - <# - .SYNOPSIS - Function to log information/error messages to output - .EXAMPLE - Show-Log "Test" - This will log the message as an Information, only if the script is run in Verbose mode - - Show-Log "Test" "Error" - This will log the message as an Error and STOP the script execution - #> - [CmdletBinding()] - param( - [Parameter()] - [ValidateNotNullOrEmpty()] - [string] - $Message, - - [Parameter()] - [ValidateNotNullOrEmpty()] - [ValidateSet('Info','Error')] - [string] - $Severity = 'Info' - ) - - begin {} - process { - if ($Severity -eq 'Info') { - if ($Verbose) { - Write-Host $Message - } - } else { - Write-Error $Message - } - } - end {} -} - -function Build-SkillArtifacts() { - <# - .SYNOPSIS - Function to compile the skill project, aggregate the project output along with its dependencies, modules, site documentation, and other files into a single distributable jar - #> - [CmdletBinding()] - [OutputType([bool])] - param() - - begin { - Show-Log "Building skill artifacts based on pom.xml." - } - process { - $DepCmd = "mvn clean org.apache.maven.plugins:maven-assembly-plugin:2.6:assembly -DdescriptorId=jar-with-dependencies package" - if (-not $Verbose) { - $DepCmd += " --quiet" - } - Invoke-Expression -Command $DepCmd | Out-String | Tee-Object -Variable 'result' - if(!($LASTEXITCODE -eq 0)) { - Show-Log "$result `n Failed to build the skill artifacts in the project." "Error" - } - return $true - } - end {} -} - -function Rename-Archive() { - <# - .SYNOPSIS - Function to rename the build archive into cli format. - #> - [CmdletBinding()] - [OutputType([bool])] - param() - - begin { - Show-Log "Renaming build archive to $OutFile." - } - process { - Move-Item -Path ./target/*jar-with-dependencies.jar -Destination $OutFile - return $? - } - end {} - -} - -#----------------[ Main Execution ]---------------------------------------------------- - -Show-Log "###########################" -Show-Log "####### Build Code ########" -Show-Log "###########################" - -if (Build-SkillArtifacts) { - Show-Log "Skill artifacts built successfully." -} - -if (-Not (Rename-Archive)) { - Show-Log "Failed to rename build archive to $OutFile" "Error" -} - -Show-Log "###########################" -Show-Log "Codebase built successfully" -Show-Log "###########################" - -exit 0 diff --git a/lib/builtins/build-flows/java-mvn/build.sh b/lib/builtins/build-flows/java-mvn/build.sh deleted file mode 100755 index 9765bdc5..00000000 --- a/lib/builtins/build-flows/java-mvn/build.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/bash -# -# Shell script for ask-cli code build for java-mvn flow. -# -# Script Usage: build.sh -# OUT_FILE is the file name for the output (required) -# DO_DEBUG is boolean value for debug logging -# -# Run this script whenever a pom.xml is defined - -readonly OUT_FILE=${1:-"upload.zip"} -readonly DO_DEBUG=${2:-false} - -main() { - if [[ $DO_DEBUG = true ]]; then - echo "###########################" - echo "####### Build Code ########" - echo "###########################" - fi - - if ! build_skill_dependencies; then - display_stderr "Failed to build the skill artifacts in the project." - exit 1 - else - [[ $DO_DEBUG == true ]] && display_debug "Dependencies built successfully." - if ! rename_to_out_file; then - display_stderr "Failed to rename the jar file to ${OUT_FILE}." - exit 1 - else - if [[ $DO_DEBUG = true ]]; then - echo "###########################" - echo "Codebase built successfully" - echo "###########################" - fi - exit 0 - fi - fi -} - -display_stderr() { - echo "[Error] $1" >&2 -} - -display_debug() { - echo "[Debug] $1" >&2 -} - -build_skill_dependencies() { - [[ $DO_DEBUG == true ]] && display_debug "Building skill artifacts based on the pom.xml." - [[ $DO_DEBUG == false ]] && QQ=true # decide if quiet flag will be appended - - mvn clean org.apache.maven.plugins:maven-assembly-plugin:2.6:assembly -DdescriptorId=jar-with-dependencies package ${QQ:+--quiet} - return $? -} - -rename_to_out_file() { - [[ $DO_DEBUG == true ]] && display_debug "Renaming the jar file (target/*jar-with-dependencies.jar) to $OUT_FILE." - mv ./target/*jar-with-dependencies.jar $OUT_FILE - return $? -} - -# Execute main function -main "$@" diff --git a/lib/builtins/build-flows/nodejs-npm.js b/lib/builtins/build-flows/nodejs-npm.js new file mode 100644 index 00000000..0f660e54 --- /dev/null +++ b/lib/builtins/build-flows/nodejs-npm.js @@ -0,0 +1,43 @@ +const fs = require('fs-extra'); +const path = require('path'); + +const AbstractBuildFlow = require('./abstract-build-flow'); + +class NodeJsNpmBuildFlow extends AbstractBuildFlow { + /** + * If this file exists than the build flow can handle build + */ + static get manifest() { return 'package.json'; } + + /** + * Returns true if the build flow can handle the build + */ + static canHandle({ src }) { + return fs.existsSync(path.join(src, NodeJsNpmBuildFlow.manifest)); + } + + /** + * Constructor + * @param {Object} options + * @param {String} options.cwd working directory for build + * @param {String} options.src source directory + * @param {String} options.buildFile full path for zip file to generate + * @param {Boolean} options.doDebug debug flag + */ + constructor({ cwd, src, buildFile, doDebug }) { + super({ cwd, src, buildFile, doDebug }); + } + + /** + * Executes build + * @param {Function} callback + */ + execute(callback) { + const quiteFlag = this.doDebug ? '' : ' --quite'; + this.debug(`Installing NodeJS dependencies based on the ${NodeJsNpmBuildFlow.manifest}.`); + this.execCommand(`npm install --production${quiteFlag}`); + this.createZip(callback); + } +} + +module.exports = NodeJsNpmBuildFlow; diff --git a/lib/builtins/build-flows/python-pip.js b/lib/builtins/build-flows/python-pip.js new file mode 100644 index 00000000..3245a4e9 --- /dev/null +++ b/lib/builtins/build-flows/python-pip.js @@ -0,0 +1,64 @@ +const childProcess = require('child_process'); +const fs = require('fs-extra'); +const path = require('path'); + +const CliError = require('@src/exceptions/cli-error'); +const AbstractBuildFlow = require('./abstract-build-flow'); + +class PythonPipBuildFlow extends AbstractBuildFlow { + /** + * If this file exists than the build flow can handle build + */ + static get manifest() { return 'requirements.txt'; } + + /** + * Returns true if the build flow can handle the build + */ + static canHandle({ src }) { + return fs.existsSync(path.join(src, PythonPipBuildFlow.manifest)); + } + + /** + * Constructor + * @param {Object} options + * @param {String} options.cwd working directory for build + * @param {String} options.src source directory + * @param {String} options.buildFile full path for zip file to generate + * @param {Boolean} options.doDebug debug flag + */ + constructor({ cwd, src, buildFile, doDebug }) { + super({ cwd, src, buildFile, doDebug }); + } + + /** + * Executes build + * @param {Function} callback + */ + execute(callback) { + const python = this.isWindows ? 'python' : 'python3'; + const pipCmdPrefix = this.isWindows ? '"venv/Scripts/pip3"' : 'venv/bin/python -m pip'; + + const { isPython2, version } = this._checkPythonVersion(python); + if (isPython2) { + return callback(new CliError(`Current python (${version}) is not supported. ` + + 'Please make sure you are using python3, or use your custom script to build the code.')); + } + this.debug('Setting up virtual environment.'); + this.execCommand(`${python} -m venv venv`); + this.debug(`Installing Python dependencies based on the ${PythonPipBuildFlow.manifest}.`); + this.execCommand(`${pipCmdPrefix} --disable-pip-version-check install -r requirements.txt -t ./`); + fs.removeSync(path.join(this.cwd, 'venv')); + // filtering out to allow consistent hashing + const filter = (entry) => entry.includes('__pycache__'); + this.createZip({ filter }, (error) => callback(error)); + } + + _checkPythonVersion(python) { + const versionStr = childProcess.spawnSync(python, ['--version']).output.toString().trim(); + const [version] = versionStr.match(/\d\.\d\.\d+/gm); + const isPython2 = version.startsWith('2.'); + return { isPython2, version }; + } +} + +module.exports = PythonPipBuildFlow; diff --git a/lib/builtins/build-flows/python-pip/build.ps1 b/lib/builtins/build-flows/python-pip/build.ps1 deleted file mode 100755 index a5480a07..00000000 --- a/lib/builtins/build-flows/python-pip/build.ps1 +++ /dev/null @@ -1,166 +0,0 @@ -#requires -version 3 -<# - .SYNOPSIS - PowerShell script for ask-cli's Python-pip code building flow. - .DESCRIPTION - This is the PowerShell version of the build script, for building the AWS Lambda deployable skill code that is written in Python language. This script is only run by the ask-cli whenever a 'requirements.txt' file is found alongside the skill code. The dependencies are installed using 'pip', and are packaged using 'zip'. - .EXAMPLE - build.ps1 archive.zip - This example showcases how to run the build script, to create an AWS Lambda deployable package called 'archive.zip'. - .EXAMPLE - build.ps1 archive.zip $true - This example showcases how to run the previous example, with additional debug information. -#> -#----------------[ Parameters ]---------------------------------------------------- -param( - [Parameter(Mandatory = $false, - ValueFromPipelineByPropertyName = $true, - HelpMessage = "Name for the AWS Lambda deployable archive")] - [ValidateNotNullOrEmpty()] - [string] - $script:OutFile = "upload.zip", - - # Provide additional debug information during script run - [Parameter(Mandatory = $false, - ValueFromPipelineByPropertyName = $true, - HelpMessage = "Enable verbose output")] - [bool] - $script:Verbose = $false -) - -#----------------[ Declarations ]---------------------------------------------------- -$ErrorActionPreference = "Stop" - -#----------------[ Functions ]---------------------------------------------------- -function Show-Log() { - <# - .SYNOPSIS - Function to log information/error messages to output - .EXAMPLE - Show-Log "Test" - This will log the message as an Information, only if the script is run in Verbose mode - - Show-Log "Test" "Error" - This will log the message as an Error and STOP the script execution - #> - [CmdletBinding()] - param( - [Parameter()] - [ValidateNotNullOrEmpty()] - [string] - $Message, - - [Parameter()] - [ValidateNotNullOrEmpty()] - [ValidateSet('Info','Error')] - [string] - $Severity = 'Info' - ) - - begin {} - process { - if ($Severity -eq 'Info') { - if ($Verbose) { - Write-Host $Message - } - } else { - Write-Error $Message - } - } - end {} -} - -function New-Py3Venv() { - <# - .SYNOPSIS - Function to create virtual environment using Python3 'venv' module. - #> - [CmdletBinding()] - [OutputType()] - param() - - begin { - Show-Log "Creating virtualenv using python3 venv." - } - process { - $PythonVersion = & python -V 2>&1 - if($PythonVersion -match "2\.\d\.\d+") { - Show-Log "Current python ($PythonVersion) is not supported. Please make sure you are using python3, or use your custom script to build the code." "Error" - } - Invoke-Expression -Command "python -m venv venv" - if(!($LASTEXITCODE -eq 0)) { - Show-Log "Failed to create python virtual environment using venv." "Error" - } - } - end {} -} - -function Install-Dependencies() { - <# - .SYNOPSIS - Function to install dependencies in requirements.txt from PyPI, in the current folder. - #> - [CmdletBinding()] - [OutputType([bool])] - param() - - begin { - Show-Log "Installing skill dependencies based on the requirements.txt." - } - process { - $DepCmd = "venv/Scripts/pip3 --disable-pip-version-check install -r requirements.txt -t ./" - if (-not $Verbose) { - $DepCmd += " -qq" - } - Invoke-Expression -Command $DepCmd - if(!($LASTEXITCODE -eq 0)) { - Show-Log "Failed to install the dependencies in the project" "Error" - } - return $true - } - end {} -} - -function Compress-Dependencies() { - <# - .SYNOPSIS - Function to compress source code and dependencies for lambda deployment. - #> - [CmdletBinding()] - [OutputType([bool])] - param() - - begin { - Show-Log "Zipping source files and dependencies to $OutFile." - } - process { - $Files = Get-ChildItem -Path .\ -Exclude venv - Compress-Archive -Path $Files -DestinationPath "$OutFile" - return $? - } - end {} - -} - -#----------------[ Main Execution ]---------------------------------------------------- - -Show-Log "###########################" -Show-Log "####### Build Code ########" -Show-Log "###########################" - -New-Py3Venv -Show-Log "Virtual environment set up successfully." - -if (Install-Dependencies) { - Show-Log "Pip install dependencies successfully." -} - -if (-Not (Compress-Dependencies)) { - Show-Log "Failed to zip the artifacts to $OutFile" "Error" -} - -Show-Log "###########################" -Show-Log "Codebase built successfully" -Show-Log "###########################" - -exit 0 diff --git a/lib/builtins/build-flows/python-pip/build.sh b/lib/builtins/build-flows/python-pip/build.sh deleted file mode 100755 index 89a4e24c..00000000 --- a/lib/builtins/build-flows/python-pip/build.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/bin/bash -# -# Shell script for ask-cli code build for python-pip flow. -# -# Script Usage: build.sh -# OUT_FILE is the file name for the output (required) -# DO_DEBUG is boolean value for debug logging -# -# Run this script whenever a requirements.txt is found - -readonly OUT_FILE=${1:-"upload.zip"} -readonly DO_DEBUG=${2:-false} - - -main() { - if [[ $DO_DEBUG == true ]]; then - echo "###########################" - echo "####### Build Code ########" - echo "###########################" - fi - - if ! create_using_python3_venv; then - display_stderr "Failed to create python virtual environment using venv." - exit 1 - fi - [[ $DO_DEBUG == true ]] && display_debug "Virtual environment set up successfully" - - if ! install_dependencies; then - display_stderr "Failed to install the dependencies in the project." - exit 1 - else - [[ $DO_DEBUG == true ]] && display_debug "Pip install dependencies successfully." - if ! zip_site_packages; then - display_stderr "Failed to zip the artifacts to ${OUT_FILE}." - exit 1 - else - if [[ $DO_DEBUG = true ]]; then - echo "###########################" - echo "Codebase built successfully" - echo "###########################" - fi - exit 0 - fi - fi -} - -display_stderr() { - echo "[Error] $1" >&2 -} - -display_debug() { - echo "[Debug] $1" >&2 -} - -####################################### -# For the version of python which contains venv, create virtual environment using the venv script. -# Arguments: -# None -# Returns: -# None -####################################### -create_using_python3_venv() { - [[ $DO_DEBUG == true ]] && display_debug "Creating virtualenv using python3 venv." - python3 -m venv venv - return $? -} - -install_dependencies() { - [[ $DO_DEBUG == true ]] && display_debug "Installing python dependencies based on the requirements.txt." - [[ $DO_DEBUG == false ]] && QQ=true # decide if quiet flag will be appended - - venv/bin/python -m pip --disable-pip-version-check install -r requirements.txt -t ./ ${QQ:+-qq} - return $? -} - -zip_site_packages() { - if [[ $DO_DEBUG = true ]]; then - display_debug "Zipping source files and dependencies to $OUT_FILE." - zip -vr "$OUT_FILE" ./* -x venv/\* - else - zip -qr "$OUT_FILE" ./* -x venv/\* - fi - return $? -} - -# Execute main function -main "$@" diff --git a/lib/builtins/build-flows/zip-only.js b/lib/builtins/build-flows/zip-only.js new file mode 100644 index 00000000..22a8d580 --- /dev/null +++ b/lib/builtins/build-flows/zip-only.js @@ -0,0 +1,32 @@ +const AbstractBuildFlow = require('./abstract-build-flow'); + +class ZipOnlyBuildFlow extends AbstractBuildFlow { + /** + * Returns true if the build flow can handle the build + */ + static canHandle() { + return true; + } + + /** + * Constructor + * @param {Object} options + * @param {String} options.cwd working directory for build + * @param {String} options.src source directory + * @param {String} options.buildFile full path for zip file to generate + * @param {Boolean} options.doDebug debug flag + */ + constructor({ cwd, src, buildFile, doDebug }) { + super({ cwd, src, buildFile, doDebug }); + } + + /** + * Executes build + * @param {Function} callback + */ + execute(callback) { + this.createZip(callback); + } +} + +module.exports = ZipOnlyBuildFlow; diff --git a/lib/controllers/skill-code-controller/code-builder.js b/lib/controllers/skill-code-controller/code-builder.js index ca713bdd..f86c49cc 100644 --- a/lib/controllers/skill-code-controller/code-builder.js +++ b/lib/controllers/skill-code-controller/code-builder.js @@ -1,32 +1,24 @@ const fs = require('fs-extra'); const path = require('path'); -const shell = require('shelljs'); -const zipUtils = require('@src/utils/zip-utils'); -const BUILT_IN_BUILD_FLOWS = [ - { - flow: 'nodejs-npm', - manifest: 'package.json' - }, - { - flow: 'python-pip', - manifest: 'requirements.txt' - }, - { - flow: 'java-mvn', - manifest: 'pom.xml' - } +const CustomBuildFlow = require('@src/builtins/build-flows/custom'); +const JavaMvnBuildFlow = require('@src/builtins/build-flows/java-mvn'); +const NodeJsNpmBuildFlow = require('@src/builtins/build-flows/nodejs-npm'); +const PythonPipBuildFlow = require('@src/builtins/build-flows/python-pip'); +const ZipOnlyBuildFlow = require('@src/builtins/build-flows/zip-only'); + +// order is important +const BUILD_FLOWS = [ + CustomBuildFlow, + NodeJsNpmBuildFlow, + JavaMvnBuildFlow, + PythonPipBuildFlow, + ZipOnlyBuildFlow ]; -const ZIP_ONLY_BUILD_FLOW = 'zip-only'; -const POWERSHELL_PREFIX = 'PowerShell.exe -Command'; class CodeBuilder { /** - * Constructor for CodeBuilder which decides the code build flow. - * Currently we resolve the packageBuillder based on OS type: - * If running in WINDOWS osType, we only supports using Powershell script; - * If not in WINDOWS, we run bash script. - * + * Constructor for CodeBuilder * @param {Object} config { src, build, doDebug }, where build = { folder, file }. */ constructor(config) { @@ -34,93 +26,34 @@ class CodeBuilder { this.src = src; this.build = build; this.doDebug = doDebug; - this.osType = process.platform === 'win32' ? 'WINDOWS' : 'UNIX'; - - this._decidePackageBuilder(); + this.BuildFlowClass = {}; + this._selectBuildFlowClass(); } /** - * Execute this.packageBuilder based on this.osType. We use powershell for Windows and regular terminal for non-Windows. + * Executes build flow * @param {Function} callback (error) */ execute(callback) { try { - // 1.reset build folder - fs.ensureDirSync(this.build.folder); - // 2.copy src code to build folder - fs.emptyDirSync(this.build.folder); - fs.copySync(path.resolve(this.src), this.build.folder, { - filter: src => !src.includes(this.build.folder) - }); + this._setUpBuildFolder(); } catch (fsErr) { - return process.nextTick(() => { - callback(fsErr); - }); - } - // 3.execute packge builder script - if (this.buildFlow !== ZIP_ONLY_BUILD_FLOW) { - let command; - if (this.osType === 'WINDOWS') { - const doDebug = this.doDebug ? '$True' : '$False'; - command = `${POWERSHELL_PREFIX} "& {& '${this.packageBuilder}' '${this.build.file}' ${doDebug} }"`; - } else { - command = `${this.packageBuilder} "${this.build.file}" ${!!this.doDebug}`; - } - shell.exec(command, { async: true, cwd: this.build.folder }, (code) => { - if (code !== 0) { - callback(`[Error]: Build Scripts failed with non-zero code: ${code}.`); - } else { - callback(); - } - }); - } else { - zipUtils.createTempZip(this.src, (zipErr, zipFilePath) => { - if (zipErr) { - return callback(zipErr); - } - fs.moveSync(zipFilePath, this.build.file, { overwrite: true }); - callback(); - }); + return process.nextTick(callback(fsErr)); } + const options = { cwd: this.build.folder, src: this.src, buildFile: this.build.file, doDebug: this.doDebug }; + const builder = new this.BuildFlowClass(options); + builder.execute((error) => callback(error)); } - /** - * Decide the buildFlow and actual packageBuilder for current instance / source code - */ - _decidePackageBuilder() { - // 1.check if user provided custom build flow script exists - const hookScriptPath = this.osType === 'WINDOWS' - ? path.join(process.cwd(), 'hooks', 'build.ps1') // TODO support non-powershell script for Windows - : path.join(process.cwd(), 'hooks', 'build.sh'); - if (fs.existsSync(hookScriptPath)) { - this.buildFlow = 'custom'; - this.packageBuilder = hookScriptPath; - return; - } - // 2.infer from the src code to decide the build flow - this.buildFlow = this._inferCodeBuildFlow(); - this.packageBuilder = this.buildFlow === ZIP_ONLY_BUILD_FLOW ? null - : path.join(__dirname, '..', '..', 'builtins', 'build-flows', this.buildFlow, this.osType === 'WINDOWS' ? 'build.ps1' : 'build.sh'); + _setUpBuildFolder() { + fs.ensureDirSync(this.build.folder); + fs.emptyDirSync(this.build.folder); + fs.copySync(path.resolve(this.src), this.build.folder, { filter: src => !src.includes(this.build.folder) }); } - /** - * Infer what is the build flow for the current src by checking the existence of manifest file in the src code. - * If no built-in build flow is found, will go by default to zip-only. - */ - _inferCodeBuildFlow() { - let buildFlowResult = ZIP_ONLY_BUILD_FLOW; // set default build flow to be 'zip-only' - if (fs.statSync(this.src).isDirectory()) { - for (const flowItem of BUILT_IN_BUILD_FLOWS) { - const { flow, manifest } = flowItem; - if (fs.existsSync(path.join(this.src, manifest))) { - buildFlowResult = flow; - break; - } - } - } - return buildFlowResult; + _selectBuildFlowClass() { + this.BuildFlowClass = BUILD_FLOWS.find(flow => flow.canHandle({ src: this.src })); } } module.exports = CodeBuilder; -module.exports.ZIP_ONLY_BUILD_FLOW = ZIP_ONLY_BUILD_FLOW; diff --git a/lib/controllers/skill-code-controller/index.js b/lib/controllers/skill-code-controller/index.js index 8ba3d960..fa54c687 100644 --- a/lib/controllers/skill-code-controller/index.js +++ b/lib/controllers/skill-code-controller/index.js @@ -35,7 +35,7 @@ module.exports = class SkillCodeController { build: codeProperty.build, doDebug: this.doDebug }); - codeProperty.buildFlow = codeBuilder.buildFlow; + codeProperty.buildFlow = codeBuilder.BuildFlowClass.name; codeBuilder.execute((execErr) => { eachCallback(execErr); }); diff --git a/lib/utils/hash-utils.js b/lib/utils/hash-utils.js index 95fc8c0c..233a0d10 100644 --- a/lib/utils/hash-utils.js +++ b/lib/utils/hash-utils.js @@ -1,7 +1,10 @@ +const crypto = require('crypto'); const folderHash = require('folder-hash'); +const fs = require('fs'); module.exports = { - getHash + getHash, + getFileHash }; /** @@ -23,3 +26,12 @@ function getHash(sourcePath, callback) { callback(error, error ? null : result.hash); }); } +/** + * Returns hash of a file + * @param{String} filePath + */ +function getFileHash(filePath) { + const sum = crypto.createHash('sha256'); + sum.update(fs.readFileSync(filePath)); + return sum.digest('hex'); +} diff --git a/package.json b/package.json index cb9e357a..9c250901 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,6 @@ "request": "^2.79.0", "rxjs": "^6.5.2", "semver": "^5.3.0", - "shelljs": "^0.8.2", "simple-git": "^1.82.0", "tmp": "^0.1.0", "uuid": "^3.4.0", diff --git a/test/integration/code-builder/code-builder-test.js b/test/integration/code-builder/code-builder-test.js new file mode 100644 index 00000000..a7cd568a --- /dev/null +++ b/test/integration/code-builder/code-builder-test.js @@ -0,0 +1,131 @@ +const { expect } = require('chai'); +const fs = require('fs-extra'); +const path = require('path'); +const sinon = require('sinon'); + +const { makeFolderInTempDirectory } = require('@test/test-utils'); +const CodeBuilder = require('@src/controllers/skill-code-controller/code-builder'); +const hashUtils = require('@src/utils/hash-utils'); + +const fixturesDirectory = path.join(process.cwd(), 'test', 'integration', 'fixtures', 'code-builder'); + +const setUpTempFolder = (sourceDirName) => { + const tempDirName = `${sourceDirName}-builder`; + const cwd = makeFolderInTempDirectory(tempDirName); + const sourceDir = path.join(fixturesDirectory, sourceDirName, 'lambda'); + const buildFolder = path.join(cwd, '.ask', 'lambda'); + const buildFile = path.join(buildFolder, 'build.zip'); + return { + src: sourceDir, + build: { folder: buildFolder, file: buildFile } + }; +}; + +describe('code builder test', () => { + it('| should build nodejs npm skill', (done) => { + const config = setUpTempFolder('nodejs-npm'); + + const codeBuilder = new CodeBuilder(config); + + const expectedMinSizeInBytes = 100000; + codeBuilder.execute((err, res) => { + expect(err).eql(null); + expect(res).eql(undefined); + const hash = hashUtils.getFileHash(config.build.file); + expect(fs.existsSync(config.build.file)).eq(true); + expect(fs.statSync(config.build.file).size).gt(expectedMinSizeInBytes); + codeBuilder.execute(() => { + const newHash = hashUtils.getFileHash(config.build.file); + expect(hash).equal(newHash); + done(); + }); + }); + }); + + it('| should build python pip skill', (done) => { + const config = setUpTempFolder('python-pip'); + + const codeBuilder = new CodeBuilder(config); + + const expectedMinSizeInBytes = 1500000; + codeBuilder.execute((err, res) => { + expect(err).eql(null); + expect(res).eql(undefined); + const hash = hashUtils.getFileHash(config.build.file); + expect(fs.existsSync(config.build.file)).eq(true); + expect(fs.statSync(config.build.file).size).gt(expectedMinSizeInBytes); + // consistent hashing on windows does not work due to chardet package + if (process.platform === 'win32') { + done(); + } else { + codeBuilder.execute(() => { + const newHash = hashUtils.getFileHash(config.build.file); + expect(hash).equal(newHash); + done(); + }); + } + }); + }); + + it('| should build java mvn skill', (done) => { + const config = setUpTempFolder('java-mvn'); + + const codeBuilder = new CodeBuilder(config); + + const expectedMinSizeInBytes = 10000000; + codeBuilder.execute((err, res) => { + expect(err).eql(null); + expect(res).eql(undefined); + const hash = hashUtils.getFileHash(config.build.file); + expect(fs.existsSync(config.build.file)).eq(true); + expect(fs.statSync(config.build.file).size).gt(expectedMinSizeInBytes); + codeBuilder.execute(() => { + const newHash = hashUtils.getFileHash(config.build.file); + expect(hash).equal(newHash); + done(); + }); + }); + }); + + it('| should build with custom script', (done) => { + const sourceDirName = 'custom'; + const config = setUpTempFolder(sourceDirName); + const sourceDir = path.join(fixturesDirectory, sourceDirName); + sinon.stub(process, 'cwd').returns(sourceDir); + + const codeBuilder = new CodeBuilder(config); + + const expectedMinSizeInBytes = 100000; + codeBuilder.execute((err, res) => { + expect(err).eql(undefined); + expect(res).eql(undefined); + expect(fs.existsSync(config.build.file)).eq(true); + expect(fs.statSync(config.build.file).size).gt(expectedMinSizeInBytes); + done(); + }); + }); + + it('| should zip only', (done) => { + const config = setUpTempFolder('zip-only'); + + const codeBuilder = new CodeBuilder(config); + + const expectedMinSizeInBytes = 100; + codeBuilder.execute((err, res) => { + expect(err).eql(null); + expect(res).eql(undefined); + const hash = hashUtils.getFileHash(config.build.file); + expect(fs.existsSync(config.build.file)).eq(true); + expect(fs.statSync(config.build.file).size).gt(expectedMinSizeInBytes); + codeBuilder.execute(() => { + const newHash = hashUtils.getFileHash(config.build.file); + expect(hash).equal(newHash); + done(); + }); + }); + }); + + afterEach(() => { + sinon.restore(); + }); +}); diff --git a/lib/builtins/build-flows/nodejs-npm/build.ps1 b/test/integration/fixtures/code-builder/custom/hooks/build.ps1 old mode 100755 new mode 100644 similarity index 100% rename from lib/builtins/build-flows/nodejs-npm/build.ps1 rename to test/integration/fixtures/code-builder/custom/hooks/build.ps1 diff --git a/lib/builtins/build-flows/nodejs-npm/build.sh b/test/integration/fixtures/code-builder/custom/hooks/build.sh similarity index 100% rename from lib/builtins/build-flows/nodejs-npm/build.sh rename to test/integration/fixtures/code-builder/custom/hooks/build.sh diff --git a/test/integration/fixtures/code-builder/custom/lambda/index.js b/test/integration/fixtures/code-builder/custom/lambda/index.js new file mode 100644 index 00000000..f7991b44 --- /dev/null +++ b/test/integration/fixtures/code-builder/custom/lambda/index.js @@ -0,0 +1 @@ +console.log('node lambda code'); diff --git a/test/integration/fixtures/code-builder/custom/lambda/package.json b/test/integration/fixtures/code-builder/custom/lambda/package.json new file mode 100644 index 00000000..13b8b29f --- /dev/null +++ b/test/integration/fixtures/code-builder/custom/lambda/package.json @@ -0,0 +1,14 @@ +{ + "name": "hello-world", + "version": "1.1.0", + "description": "test", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Amazon Alexa", + "license": "ISC", + "dependencies": { + "ask-sdk-core": "^2.6.0" + } + } diff --git a/test/integration/fixtures/code-builder/java-mvn/lambda/pom.xml b/test/integration/fixtures/code-builder/java-mvn/lambda/pom.xml new file mode 100644 index 00000000..67b0585e --- /dev/null +++ b/test/integration/fixtures/code-builder/java-mvn/lambda/pom.xml @@ -0,0 +1,56 @@ + + 4.0.0 + alexa-skills-kit-samples + helloworld + jar + 1.0 + helloworld + http://developer.amazon.com/ask + + + The Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + Alexa Skills Kit + ask-sdk-java@amazon.com + Alexa + http://developer.amazon.com/ask + + + + scm:git:https://github.com/amzn/alexa-skills-kit-java.git + scm:git:https://github.com/amzn/alexa-skills-kit-java.git + https://github.com/amzn/alexa-skills-kit-java.git + + + + + com.amazon.alexa + ask-sdk + 2.20.2 + + + + + src + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.7.0 + + 1.8 + 1.8 + true + + + + + + + diff --git a/test/integration/fixtures/code-builder/java-mvn/lambda/src/com/amazon/ask/helloworld/HelloWorldStreamHandler.java b/test/integration/fixtures/code-builder/java-mvn/lambda/src/com/amazon/ask/helloworld/HelloWorldStreamHandler.java new file mode 100644 index 00000000..26ba7faf --- /dev/null +++ b/test/integration/fixtures/code-builder/java-mvn/lambda/src/com/amazon/ask/helloworld/HelloWorldStreamHandler.java @@ -0,0 +1,28 @@ +/* + Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file + except in compliance with the License. A copy of the License is located at + http://aws.amazon.com/apache2.0/ + or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for + the specific language governing permissions and limitations under the License. +*/ + +package com.amazon.ask.helloworld; + +import com.amazon.ask.Skill; +import com.amazon.ask.Skills; +import com.amazon.ask.SkillStreamHandler; + +public class HelloWorldStreamHandler extends SkillStreamHandler { + + private static Skill getSkill() { + return Skills.standard() + .build(); + } + + public HelloWorldStreamHandler() { + super(getSkill()); + } + +} diff --git a/test/integration/fixtures/code-builder/nodejs-npm/lambda/index.js b/test/integration/fixtures/code-builder/nodejs-npm/lambda/index.js new file mode 100644 index 00000000..f7991b44 --- /dev/null +++ b/test/integration/fixtures/code-builder/nodejs-npm/lambda/index.js @@ -0,0 +1 @@ +console.log('node lambda code'); diff --git a/test/integration/fixtures/code-builder/nodejs-npm/lambda/package.json b/test/integration/fixtures/code-builder/nodejs-npm/lambda/package.json new file mode 100644 index 00000000..55763134 --- /dev/null +++ b/test/integration/fixtures/code-builder/nodejs-npm/lambda/package.json @@ -0,0 +1,14 @@ +{ + "name": "hello-world", + "version": "1.1.0", + "description": "test", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Amazon Alexa", + "license": "ISC", + "dependencies": { + "ask-sdk-core": "^2.6.0" + } +} diff --git a/test/integration/fixtures/code-builder/python-pip/lambda/hello_world.py b/test/integration/fixtures/code-builder/python-pip/lambda/hello_world.py new file mode 100644 index 00000000..e203b952 --- /dev/null +++ b/test/integration/fixtures/code-builder/python-pip/lambda/hello_world.py @@ -0,0 +1 @@ +print("python lambda code") diff --git a/test/integration/fixtures/code-builder/python-pip/lambda/requirements.txt b/test/integration/fixtures/code-builder/python-pip/lambda/requirements.txt new file mode 100644 index 00000000..4a789fe6 --- /dev/null +++ b/test/integration/fixtures/code-builder/python-pip/lambda/requirements.txt @@ -0,0 +1 @@ +ask-sdk-core>=1.10.2 diff --git a/test/integration/fixtures/code-builder/zip-only/lambda/some-file.txt b/test/integration/fixtures/code-builder/zip-only/lambda/some-file.txt new file mode 100644 index 00000000..f0eec86f --- /dev/null +++ b/test/integration/fixtures/code-builder/zip-only/lambda/some-file.txt @@ -0,0 +1 @@ +some content \ No newline at end of file diff --git a/test/integration/run-test.js b/test/integration/run-test.js index 54d63b19..e79da283 100644 --- a/test/integration/run-test.js +++ b/test/integration/run-test.js @@ -2,9 +2,15 @@ require('module-alias/register'); process.env.ASK_SHARE_USAGE = false; -[ - '@test/integration/commands/smapi-commands-test.js' -].forEach((testFile) => { +const tests = [ + '@test/integration/code-builder/code-builder-test.js' +]; + +if (process.platform !== 'win32') { + // smapi tests are not supported on windows + tests.push('@test/integration/commands/smapi-commands-test.js'); +} +tests.forEach((testFile) => { // eslint-disable-next-line global-require require(testFile); }); diff --git a/test/test-utils.js b/test/test-utils.js index 180cc75a..c53ad67c 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -29,7 +29,9 @@ const deleteFolderInTempDirectory = (folder) => { const getPathInTempDirectory = (folderPath) => path.join(tempDirectory, folderPath); const makeFolderInTempDirectory = (folderPath) => { - fs.ensureDirSync(getPathInTempDirectory(folderPath)); + const fullPath = getPathInTempDirectory(folderPath); + fs.ensureDirSync(fullPath); + return fullPath; }; const run = (cmd, args, options = {}) => { diff --git a/test/unit/builtins/build-flows/abstract-build-flow-test.js b/test/unit/builtins/build-flows/abstract-build-flow-test.js new file mode 100644 index 00000000..d39f83d1 --- /dev/null +++ b/test/unit/builtins/build-flows/abstract-build-flow-test.js @@ -0,0 +1,100 @@ +const { expect } = require('chai'); +const childProcess = require('child_process'); +const fs = require('fs-extra'); +const path = require('path'); +const sinon = require('sinon'); + +const { makeFolderInTempDirectory } = require('@test/test-utils'); +const AbstractBuildFlow = require('@src/builtins/build-flows/abstract-build-flow'); +const Messenger = require('@src/view/messenger'); + +const fixturesDirectory = path.join(process.cwd(), 'test', 'unit', 'fixture', 'build-flow'); + +describe('AbstractBuildFlow test', () => { + let config; + let debugStub; + beforeEach(() => { + const cwd = makeFolderInTempDirectory('abstract-build-flow'); + const buildFile = path.join(cwd, 'build.zip'); + fs.copySync(fixturesDirectory, cwd); + config = { + cwd, + src: 'src', + buildFile, + doDebug: false + }; + debugStub = sinon.stub(); + sinon.stub(Messenger, 'getInstance').returns({ debug: debugStub }); + }); + + describe('# inspect correctness for constructor', () => { + it('| initiate the class', () => { + const buildFlow = new AbstractBuildFlow(config); + + expect(buildFlow).to.be.instanceOf(AbstractBuildFlow); + }); + }); + + describe('# inspect correctness of createZip and modifyZip', () => { + it('| should call create and modify temp zip', (done) => { + const buildFlow = new AbstractBuildFlow(config); + + buildFlow.createZip((err, res) => { + expect(err).eql(null); + expect(res).eql(''); + buildFlow.modifyZip((errModify, resModify) => { + expect(errModify).eql(null); + expect(resModify).eql(''); + done(); + }); + }); + }); + + it('| should call create and modify temp zip with options', (done) => { + const buildFlow = new AbstractBuildFlow(config); + + buildFlow.createZip({ filter: () => false }, (err, res) => { + expect(err).eql(null); + expect(res).eql(''); + buildFlow.modifyZip({ entryProcessor: () => {} }, (errModify, resModify) => { + expect(errModify).eql(null); + expect(resModify).eql(''); + done(); + }); + }); + }); + }); + + describe('# inspect correctness of execCommand', () => { + it('| should execute the command', () => { + const testCommand = 'test'; + const stub = sinon.stub(childProcess, 'execSync'); + const buildFlow = new AbstractBuildFlow(config); + + buildFlow.execCommand(testCommand); + expect(stub.callCount).eql(1); + expect(stub.args[0][0]).eql(testCommand); + }); + }); + + describe('# inspect correctness of debug', () => { + it('| should not output debug message', () => { + const buildFlow = new AbstractBuildFlow(config); + + buildFlow.debug('test'); + expect(debugStub.callCount).eql(0); + }); + + it('| should output debug message', () => { + config.doDebug = true; + const buildFlow = new AbstractBuildFlow(config); + + buildFlow.debug('test'); + expect(debugStub.callCount).eql(1); + }); + }); + + afterEach(() => { + sinon.restore(); + }); +}); diff --git a/test/unit/builtins/build-flows/custom-test.js b/test/unit/builtins/build-flows/custom-test.js new file mode 100644 index 00000000..fc9d3961 --- /dev/null +++ b/test/unit/builtins/build-flows/custom-test.js @@ -0,0 +1,96 @@ +const { expect } = require('chai'); +const path = require('path'); +const sinon = require('sinon'); + +const AbstractBuildFlow = require('@src/builtins/build-flows/abstract-build-flow'); +const CustomBuildFlow = require('@src/builtins/build-flows/custom'); + +describe('CustomBuildFlow test', () => { + let config; + let execStub; + let debugStub; + let platformStub; + beforeEach(() => { + config = { + cwd: 'cwd', + src: 'src', + buildFile: 'buildFile', + doDebug: false + }; + sinon.stub(path, 'join').returns('some-script'); + execStub = sinon.stub(AbstractBuildFlow.prototype, 'execCommand'); + debugStub = sinon.stub(AbstractBuildFlow.prototype, 'debug'); + platformStub = sinon.stub(process, 'platform').value('darwin'); + }); + describe('# inspect correctness of execute', () => { + it('| should execute commands', (done) => { + const buildFlow = new CustomBuildFlow(config); + + buildFlow.execute((err, res) => { + expect(err).eql(undefined); + expect(res).eql(undefined); + expect(execStub.args[0][0]).eql('some-script "buildFile" false'); + done(); + }); + }); + + it('| should execute commands on windows', (done) => { + platformStub.value('win32'); + const buildFlow = new CustomBuildFlow(config); + + buildFlow.execute((err, res) => { + expect(err).eql(undefined); + expect(res).eql(undefined); + expect(execStub.args[0][0]).eql('PowerShell.exe -Command "& {& \'some-script\' \'buildFile\' $False }"'); + done(); + }); + }); + + it('| should execute commands with debug', (done) => { + config.doDebug = true; + const buildFlow = new CustomBuildFlow(config); + + buildFlow.execute((err, res) => { + expect(err).eql(undefined); + expect(res).eql(undefined); + expect(execStub.args[0][0]).eql('some-script "buildFile" true'); + expect(debugStub.args[0][0]).eql('Executing custom hook script some-script.'); + done(); + }); + }); + + it('| should execute commands with debug on windows', (done) => { + platformStub.value('win32'); + config.doDebug = true; + const buildFlow = new CustomBuildFlow(config); + + buildFlow.execute((err, res) => { + expect(err).eql(undefined); + expect(res).eql(undefined); + expect(execStub.args[0][0]).eql('PowerShell.exe -Command "& {& \'some-script\' \'buildFile\' $True }"'); + expect(debugStub.args[0][0]).eql('Executing custom hook script some-script.'); + done(); + }); + }); + }); + + describe('# inspect correctness of manifest getter', () => { + it('| should return manifest script for windows', () => { + platformStub.value('win32'); + + const { manifest } = CustomBuildFlow; + + expect(manifest).eq('build.ps1'); + }); + + it('| should return manifest script for non windows', () => { + const { manifest } = CustomBuildFlow; + + expect(manifest).eq('build.sh'); + }); + }); + + afterEach(() => { + sinon.restore(); + }); +}); diff --git a/test/unit/builtins/build-flows/java-mvn-test.js b/test/unit/builtins/build-flows/java-mvn-test.js new file mode 100644 index 00000000..17819540 --- /dev/null +++ b/test/unit/builtins/build-flows/java-mvn-test.js @@ -0,0 +1,85 @@ +const { expect } = require('chai'); +const fs = require('fs-extra'); +const path = require('path'); +const sinon = require('sinon'); + +const AbstractBuildFlow = require('@src/builtins/build-flows/abstract-build-flow'); +const JavaMvnBuildFlow = require('@src/builtins/build-flows/java-mvn'); + +describe('JavaMvnBuildFlow test', () => { + let config; + let execStub; + let debugStub; + beforeEach(() => { + config = { + cwd: 'cwd', + src: 'src', + buildFile: 'buildFile', + doDebug: false + }; + sinon.stub(fs, 'moveSync'); + sinon.stub(fs, 'readdirSync').returns([]); + sinon.stub(path, 'join').returns('some-path'); + sinon.stub(AbstractBuildFlow.prototype, 'modifyZip').yields(); + execStub = sinon.stub(AbstractBuildFlow.prototype, 'execCommand'); + debugStub = sinon.stub(AbstractBuildFlow.prototype, 'debug'); + }); + describe('# inspect correctness of execute', () => { + it('| should execute commands', (done) => { + const buildFlow = new JavaMvnBuildFlow(config); + + buildFlow.execute((err, res) => { + expect(err).eql(undefined); + expect(res).eql(undefined); + expect(execStub.args[0][0]).eql('mvn clean org.apache.maven.plugins:maven-' + + 'assembly-plugin:2.6:assembly -DdescriptorId=jar-with-dependencies package'); + done(); + }); + }); + + it('| should execute commands with debug', (done) => { + config.doDebug = true; + const buildFlow = new JavaMvnBuildFlow(config); + + buildFlow.execute((err, res) => { + expect(err).eql(undefined); + expect(res).eql(undefined); + expect(execStub.args[0][0]).eql('mvn clean org.apache.maven.plugins:maven-' + + 'assembly-plugin:2.6:assembly -DdescriptorId=jar-with-dependencies package'); + expect(debugStub.args[0][0]).eql('Building skill artifacts based on the pom.xml.'); + expect(debugStub.args[1][0]).eql('Renaming the jar file some-path to buildFile.'); + done(); + }); + }); + }); + + describe('# inspect correctness of removeCommentsFromPomProperties', () => { + it('| should remove comments from pom properties file', () => { + const entry = { + entryName: 'pom.properties', + getData: () => '# some comment\n# generated at Thu Oct 22 2020 12:28:31\nsome value\nanother-value', + setData: () => {} + }; + const setDataSpy = sinon.spy(entry, 'setData'); + const buildFlow = new JavaMvnBuildFlow(config); + buildFlow._removeCommentsFromPomProperties(entry); + expect(setDataSpy.args[0][0]).eq('some value\nanother-value'); + }); + + it('| should note remove comments from non pom properties file', () => { + const entry = { + entryName: 'other.properties', + getData: () => '# some comment\n# generated at Thu Oct 22 2020 12:28:31\nsome value\nanother-value', + setData: () => {} + }; + const setDataSpy = sinon.spy(entry, 'setData'); + const buildFlow = new JavaMvnBuildFlow(config); + buildFlow._removeCommentsFromPomProperties(entry); + expect(setDataSpy.callCount).eq(0); + }); + }); + + afterEach(() => { + sinon.restore(); + }); +}); diff --git a/test/unit/builtins/build-flows/nodejs-npm-test.js b/test/unit/builtins/build-flows/nodejs-npm-test.js new file mode 100644 index 00000000..91526bce --- /dev/null +++ b/test/unit/builtins/build-flows/nodejs-npm-test.js @@ -0,0 +1,53 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); + +const AbstractBuildFlow = require('@src/builtins/build-flows/abstract-build-flow'); +const NodeJsNpmBuildFlow = require('@src/builtins/build-flows/nodejs-npm'); + +describe('NodeJsNpmBuildFlow test', () => { + let config; + let execStub; + let debugStub; + let createZipStub; + beforeEach(() => { + config = { + cwd: 'cwd', + src: 'src', + buildFile: 'buildFile', + doDebug: false + }; + execStub = sinon.stub(AbstractBuildFlow.prototype, 'execCommand'); + debugStub = sinon.stub(AbstractBuildFlow.prototype, 'debug'); + createZipStub = sinon.stub(AbstractBuildFlow.prototype, 'createZip').yields(); + }); + describe('# inspect correctness of execute', () => { + it('| should execute commands', (done) => { + const buildFlow = new NodeJsNpmBuildFlow(config); + + buildFlow.execute((err, res) => { + expect(err).eql(undefined); + expect(res).eql(undefined); + expect(execStub.args[0][0]).eql('npm install --production --quite'); + expect(createZipStub.callCount).eql(1); + done(); + }); + }); + + it('| should execute commands with debug flag', (done) => { + config.doDebug = true; + const buildFlow = new NodeJsNpmBuildFlow(config); + + buildFlow.execute((err, res) => { + expect(err).eql(undefined); + expect(res).eql(undefined); + expect(execStub.args[0][0]).eql('npm install --production'); + expect(debugStub.args[0][0]).eql('Installing NodeJS dependencies based on the package.json.'); + done(); + }); + }); + }); + + afterEach(() => { + sinon.restore(); + }); +}); diff --git a/test/unit/builtins/build-flows/python-pip-test.js b/test/unit/builtins/build-flows/python-pip-test.js new file mode 100644 index 00000000..a6a03a5d --- /dev/null +++ b/test/unit/builtins/build-flows/python-pip-test.js @@ -0,0 +1,92 @@ +const { expect } = require('chai'); +const childProcess = require('child_process'); +const sinon = require('sinon'); + +const AbstractBuildFlow = require('@src/builtins/build-flows/abstract-build-flow'); +const PythonPipBuildFlow = require('@src/builtins/build-flows/python-pip'); + +describe('PythonPipBuildFlow test', () => { + let config; + let execStub; + let debugStub; + let createZipStub; + let platformStub; + let checkVersionStub; + beforeEach(() => { + config = { + cwd: 'cwd', + src: 'src', + buildFile: 'buildFile', + doDebug: false + }; + execStub = sinon.stub(AbstractBuildFlow.prototype, 'execCommand'); + debugStub = sinon.stub(AbstractBuildFlow.prototype, 'debug'); + createZipStub = sinon.stub(AbstractBuildFlow.prototype, 'createZip').yields(); + platformStub = sinon.stub(process, 'platform').value('darwin'); + checkVersionStub = sinon.stub(childProcess, 'spawnSync').returns({ output: ', Python 3.7.2 ,' }); + }); + describe('# inspect correctness of execute', () => { + it('| should execute commands', (done) => { + const buildFlow = new PythonPipBuildFlow(config); + + buildFlow.execute((err, res) => { + expect(err).eql(undefined); + expect(res).eql(undefined); + expect(execStub.args[0][0]).eql('python3 -m venv venv'); + expect(execStub.args[1][0]).eql('venv/bin/python -m pip --disable-pip-version-check install -r requirements.txt -t ./'); + expect(createZipStub.callCount).eql(1); + expect(checkVersionStub.callCount).eql(1); + done(); + }); + }); + + it('| should execute commands for windows', (done) => { + platformStub.value('win32'); + const buildFlow = new PythonPipBuildFlow(config); + + buildFlow.execute((err, res) => { + expect(err).eql(undefined); + expect(res).eql(undefined); + expect(execStub.args[0][0]).eql('python -m venv venv'); + expect(execStub.args[1][0]).eql('"venv/Scripts/pip3" --disable-pip-version-check install -r requirements.txt -t ./'); + expect(createZipStub.callCount).eql(1); + expect(checkVersionStub.callCount).eql(1); + done(); + }); + }); + + it('| should execute commands with debug flag', (done) => { + config.doDebug = true; + const buildFlow = new PythonPipBuildFlow(config); + + buildFlow.execute((err, res) => { + expect(err).eql(undefined); + expect(res).eql(undefined); + expect(execStub.args[0][0]).eql('python3 -m venv venv'); + expect(execStub.args[1][0]).eql('venv/bin/python -m pip --disable-pip-version-check install -r requirements.txt -t ./'); + expect(debugStub.args[0][0]).eql('Setting up virtual environment.'); + expect(debugStub.args[1][0]).eql('Installing Python dependencies based on the requirements.txt.'); + expect(checkVersionStub.callCount).eql(1); + done(); + }); + }); + + it('| should throw error when python 2 is used', (done) => { + checkVersionStub.returns({ output: ', Python 2.7.2 ,' }); + const buildFlow = new PythonPipBuildFlow(config); + + buildFlow.execute((err, res) => { + expect(err.message).eql('Current python (2.7.2) is not supported. ' + + 'Please make sure you are using python3, or use your custom script to build the code.'); + expect(res).eql(undefined); + expect(checkVersionStub.callCount).eql(1); + expect(createZipStub.callCount).eql(0); + done(); + }); + }); + }); + + afterEach(() => { + sinon.restore(); + }); +}); diff --git a/test/unit/builtins/build-flows/zip-only-test.js b/test/unit/builtins/build-flows/zip-only-test.js new file mode 100644 index 00000000..1d9b8d9e --- /dev/null +++ b/test/unit/builtins/build-flows/zip-only-test.js @@ -0,0 +1,35 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); + +const AbstractBuildFlow = require('@src/builtins/build-flows/abstract-build-flow'); +const ZipOnlyBuildFlow = require('@src/builtins/build-flows/zip-only'); + +describe('ZipOnlyBuildFlow test', () => { + let config; + let createZipStub; + beforeEach(() => { + config = { + cwd: 'cwd', + src: 'src', + buildFile: 'buildFile', + doDebug: false + }; + createZipStub = sinon.stub(AbstractBuildFlow.prototype, 'createZip').yields(); + }); + describe('# inspect correctness of execute', () => { + it('| should execute commands', (done) => { + const buildFlow = new ZipOnlyBuildFlow(config); + + buildFlow.execute((err, res) => { + expect(err).eql(undefined); + expect(res).eql(undefined); + expect(createZipStub.callCount).eql(1); + done(); + }); + }); + }); + + afterEach(() => { + sinon.restore(); + }); +}); diff --git a/test/unit/controller/code-builder-test.js b/test/unit/controller/code-builder-test.js index e9b51536..37527d84 100644 --- a/test/unit/controller/code-builder-test.js +++ b/test/unit/controller/code-builder-test.js @@ -2,322 +2,87 @@ const { expect } = require('chai'); const sinon = require('sinon'); const fs = require('fs-extra'); const path = require('path'); -const shell = require('shelljs'); +const ZipOnlyBuildFlow = require('@src/builtins/build-flows/zip-only'); +const NodeJsNpmBuildFlow = require('@src/builtins/build-flows/nodejs-npm'); const CodeBuilder = require('@src/controllers/skill-code-controller/code-builder'); -const zipUtils = require('@src/utils/zip-utils'); describe('Controller test - CodeBuilder test', () => { - const TEST_SRC = 'src'; - const TEST_BUILD_FILE = 'buildFile'; - const TEST_BUILD_FOLDER = 'buildFolder'; - const TEST_BUILD = { file: TEST_BUILD_FILE, folder: TEST_BUILD_FOLDER }; - const TEST_DO_DEBUG = false; - const TEST_ZIP_FILE_PATH = 'zipFilePath'; - const TEST_CONFIGURATION = { - src: TEST_SRC, - build: TEST_BUILD, - doDebug: TEST_DO_DEBUG + const config = { + src: 'src', + build: 'buildFile', + doDebug: false }; + let selectBuildFlowClassStub; + beforeEach(() => { + sinon.stub(fs, 'ensureDirSync'); + sinon.stub(fs, 'emptyDirSync'); + sinon.stub(fs, 'copySync'); + sinon.stub(path, 'resolve'); + }); describe('# inspect correctness for constructor', () => { - afterEach(() => { - sinon.restore(); - }); - - it('| initiate as a CodeBuilder class and OS is on win32', () => { - // setup - sinon.stub(CodeBuilder.prototype, '_decidePackageBuilder'); - sinon.stub(process, 'platform').value('win32'); - // call - const codeBuilder = new CodeBuilder(TEST_CONFIGURATION); - // verify - expect(codeBuilder).to.be.instanceOf(CodeBuilder); - expect(codeBuilder.src).equal(TEST_SRC); - expect(codeBuilder.build).deep.equal(TEST_BUILD); - expect(codeBuilder.doDebug).equal(TEST_DO_DEBUG); - expect(codeBuilder.osType).equal('WINDOWS'); - }); - - it('| initiate as a CodeBuilder class and OS is on non-win32', () => { - // setup - sinon.stub(CodeBuilder.prototype, '_decidePackageBuilder'); - sinon.stub(process, 'platform').value('anyOther'); - // call - const codeBuilder = new CodeBuilder(TEST_CONFIGURATION); - // verify - expect(codeBuilder).to.be.instanceOf(CodeBuilder); - expect(codeBuilder.src).equal(TEST_SRC); - expect(codeBuilder.build).deep.equal(TEST_BUILD); - expect(codeBuilder.doDebug).equal(TEST_DO_DEBUG); - expect(codeBuilder.osType).equal('UNIX'); - }); - - it('| decide build flow as custom hook script during CodeBuilder instantiating on win32', () => { - // setup - sinon.stub(process, 'platform').value('win32'); - sinon.stub(fs, 'existsSync').returns(true); - // call - const codeBuilder = new CodeBuilder(TEST_CONFIGURATION); - // verify - expect(codeBuilder).to.be.instanceOf(CodeBuilder); - expect(codeBuilder.src).equal(TEST_SRC); - expect(codeBuilder.build).deep.equal(TEST_BUILD); - expect(codeBuilder.doDebug).equal(TEST_DO_DEBUG); - expect(codeBuilder.osType).equal('WINDOWS'); - expect(codeBuilder.buildFlow).equal('custom'); - expect(codeBuilder.packageBuilder).equal(path.join(process.cwd(), 'hooks', 'build.ps1')); - }); - - it('| decide build flow as custom hook script during CodeBuilder instantiating on non-win32', () => { - // setup - sinon.stub(process, 'platform').value('non-win32'); - sinon.stub(fs, 'existsSync').returns(true); - // call - const codeBuilder = new CodeBuilder(TEST_CONFIGURATION); - // verify - expect(codeBuilder).to.be.instanceOf(CodeBuilder); - expect(codeBuilder.src).equal(TEST_SRC); - expect(codeBuilder.build).deep.equal(TEST_BUILD); - expect(codeBuilder.doDebug).equal(TEST_DO_DEBUG); - expect(codeBuilder.osType).equal('UNIX'); - expect(codeBuilder.buildFlow).equal('custom'); - expect(codeBuilder.packageBuilder).equal(path.join(process.cwd(), 'hooks', 'build.sh')); - }); + it('| initiate CodeBuilder', () => { + selectBuildFlowClassStub = sinon.stub(CodeBuilder.prototype, '_selectBuildFlowClass'); + const codeBuilder = new CodeBuilder(config); - it('| decide build flow as builtin nodejs codebase during CodeBuilder instantiating on win32', () => { - // setup - const TEST_HOOK_SCRIPT = path.join(process.cwd(), 'hooks', 'build.ps1'); - sinon.stub(process, 'platform').value('win32'); - sinon.stub(fs, 'existsSync'); - fs.existsSync.withArgs(TEST_HOOK_SCRIPT).returns(false); - fs.existsSync.withArgs(path.join(TEST_SRC, 'requirements.txt')).returns(true); - sinon.stub(fs, 'statSync').returns({ - isDirectory: () => true - }); - // call - const codeBuilder = new CodeBuilder(TEST_CONFIGURATION); - // verify expect(codeBuilder).to.be.instanceOf(CodeBuilder); - expect(codeBuilder.src).equal(TEST_SRC); - expect(codeBuilder.build).deep.equal(TEST_BUILD); - expect(codeBuilder.doDebug).equal(TEST_DO_DEBUG); - expect(codeBuilder.osType).equal('WINDOWS'); - expect(codeBuilder.buildFlow).equal('python-pip'); - expect(codeBuilder.packageBuilder.endsWith(`build-flows${path.sep}python-pip${path.sep}build.ps1`)).equal(true); - }); - - it('| decide build flow as builtin nodejs codebase during CodeBuilder instantiating on non-win32', () => { - // setup - const TEST_HOOK_SCRIPT = path.join(process.cwd(), 'hooks', 'build.sh'); - sinon.stub(process, 'platform').value('non-win32'); - sinon.stub(fs, 'existsSync'); - fs.existsSync.withArgs(TEST_HOOK_SCRIPT).returns(false); - fs.existsSync.withArgs(path.join(TEST_SRC, 'pom.xml')).returns(true); - sinon.stub(fs, 'statSync').returns({ - isDirectory: () => true - }); - // call - const codeBuilder = new CodeBuilder(TEST_CONFIGURATION); - // verify - expect(codeBuilder).to.be.instanceOf(CodeBuilder); - expect(codeBuilder.src).equal(TEST_SRC); - expect(codeBuilder.build).deep.equal(TEST_BUILD); - expect(codeBuilder.doDebug).equal(TEST_DO_DEBUG); - expect(codeBuilder.osType).equal('UNIX'); - expect(codeBuilder.buildFlow).equal('java-mvn'); - expect(codeBuilder.packageBuilder.endsWith(`build-flows${path.sep}java-mvn${path.sep}build.sh`)).equal(true); - }); - - it('| decide build flow as zip-only build flow during CodeBuilder instantiating on win32', () => { - // setup - const TEST_HOOK_SCRIPT = path.join(process.cwd(), 'hooks', 'build.ps1'); - sinon.stub(process, 'platform').value('win32'); - sinon.stub(fs, 'existsSync'); - fs.existsSync.withArgs(TEST_HOOK_SCRIPT).returns(false); - sinon.stub(fs, 'statSync').returns({ - isDirectory: () => false - }); - // call - const codeBuilder = new CodeBuilder(TEST_CONFIGURATION); - // verify - expect(codeBuilder).to.be.instanceOf(CodeBuilder); - expect(codeBuilder.src).equal(TEST_SRC); - expect(codeBuilder.build).deep.equal(TEST_BUILD); - expect(codeBuilder.doDebug).equal(TEST_DO_DEBUG); - expect(codeBuilder.osType).equal('WINDOWS'); - expect(codeBuilder.buildFlow).equal('zip-only'); - expect(codeBuilder.packageBuilder).equal(null); - }); - - it('| decide build flow as zip-only build flow during CodeBuilder instantiating on non-win32', () => { - // setup - const TEST_HOOK_SCRIPT = path.join(process.cwd(), 'hooks', 'build.sh'); - sinon.stub(process, 'platform').value('non-win32'); - sinon.stub(fs, 'existsSync'); - fs.existsSync.withArgs(TEST_HOOK_SCRIPT).returns(false); - sinon.stub(fs, 'statSync').returns({ - isDirectory: () => false - }); - // call - const codeBuilder = new CodeBuilder(TEST_CONFIGURATION); - // verify - expect(codeBuilder).to.be.instanceOf(CodeBuilder); - expect(codeBuilder.src).equal(TEST_SRC); - expect(codeBuilder.build).deep.equal(TEST_BUILD); - expect(codeBuilder.doDebug).equal(TEST_DO_DEBUG); - expect(codeBuilder.osType).equal('UNIX'); - expect(codeBuilder.buildFlow).equal('zip-only'); - expect(codeBuilder.packageBuilder).equal(null); + expect(codeBuilder.src).equal(config.src); + expect(codeBuilder.build).deep.equal(config.build); + expect(codeBuilder.doDebug).equal(config.doDebug); + expect(selectBuildFlowClassStub.callCount).equal(1); }); }); - describe('# test class method: execute', () => { - const TEST_BUILD_FLOW = 'buildFlow'; - const TEST_PACKAGE_BUILDER = 'packageBuilder'; - let codeBuilder; - - beforeEach(() => { - sinon.stub(CodeBuilder.prototype, '_decidePackageBuilder'); - codeBuilder = new CodeBuilder(TEST_CONFIGURATION); - codeBuilder.buildFlow = TEST_BUILD_FLOW; - codeBuilder.packageBuilder = TEST_PACKAGE_BUILDER; - }); - - afterEach(() => { - sinon.restore(); - }); - - it('| execute fails when working on the fs on win32, expect error called back', (done) => { - // setup - codeBuilder.osType = 'WINDOWS'; - sinon.stub(fs, 'ensureDirSync'); - sinon.stub(fs, 'emptyDirSync'); - sinon.stub(fs, 'copySync').throws(new Error('error')); - // call - codeBuilder.execute((err) => { - // verify - expect(err.message).equal('error'); - expect(fs.ensureDirSync.args[0][0]).equal(TEST_BUILD_FOLDER); - expect(fs.emptyDirSync.args[0][0]).equal(TEST_BUILD_FOLDER); + describe('# inspect correctness of execute method', () => { + it('| should execute zip only build flow when no other build flow is found', (done) => { + sinon.stub(path, 'join'); + sinon.stub(fs, 'existsSync').returns(false); + const executeStub = sinon.stub(ZipOnlyBuildFlow.prototype, 'execute').yields(); + const codeBuilder = new CodeBuilder(config); + + codeBuilder.execute((err, res) => { + expect(err).eql(undefined); + expect(res).eql(undefined); + expect(codeBuilder.BuildFlowClass.name).equal('ZipOnlyBuildFlow'); + expect(executeStub.callCount).equal(1); done(); }); }); - it('| execute fails when working on the fs on non-win32, expect error called back', (done) => { - // setup - codeBuilder.osType = 'UNIX'; - sinon.stub(fs, 'ensureDirSync').throws(new Error('error')); - sinon.stub(fs, 'emptyDirSync'); - sinon.stub(fs, 'copySync'); - // call - codeBuilder.execute((err) => { - // verify - expect(err.message).equal('error'); + it('| should execute node js npm build flow', (done) => { + sinon.stub(path, 'join') + .returns('some-file') + .withArgs(sinon.match.any, sinon.match('package.json')).returns('package.json'); + sinon.stub(fs, 'existsSync') + .returns(false) + .withArgs(sinon.match('package.json')).returns(true); + const executeStub = sinon.stub(NodeJsNpmBuildFlow.prototype, 'execute').yields(); + const codeBuilder = new CodeBuilder(config); + + codeBuilder.execute((err, res) => { + expect(err).eql(undefined); + expect(res).eql(undefined); + expect(codeBuilder.BuildFlowClass.name).equal('NodeJsNpmBuildFlow'); + expect(executeStub.callCount).equal(1); done(); }); }); - it('| shell execute fails on win32, expect error called back', (done) => { - // setup - const TEST_CODE = 10; - codeBuilder.osType = 'WINDOWS'; - sinon.stub(fs, 'ensureDirSync'); - sinon.stub(fs, 'emptyDirSync'); - sinon.stub(fs, 'copySync'); - sinon.stub(shell, 'exec').callsArgWith(2, TEST_CODE, null, 'error'); - // call - codeBuilder.execute((err) => { - // verify - expect(shell.exec.args[0][0]).equal(`PowerShell.exe -Command "& {& '${TEST_PACKAGE_BUILDER}' '${TEST_BUILD_FILE}' $False }"`); - expect(err).equal(`[Error]: Build Scripts failed with non-zero code: ${TEST_CODE}.`); - done(); - }); - }); - - it('| shell execute fails on non-win32, expect error called back', (done) => { - // setup - const TEST_CODE = 10; - codeBuilder.osType = 'UNIX'; - sinon.stub(fs, 'ensureDirSync'); - sinon.stub(fs, 'emptyDirSync'); - sinon.stub(fs, 'copySync'); - sinon.stub(shell, 'exec').callsArgWith(2, TEST_CODE, null, 'error'); - // call - codeBuilder.execute((err) => { - // verify - expect(shell.exec.args[0][0]).equal(`${TEST_PACKAGE_BUILDER} "${TEST_BUILD_FILE}" false`); - expect(err).equal(`[Error]: Build Scripts failed with non-zero code: ${TEST_CODE}.`); - done(); - }); - }); + it('| should throw error when unable to set up build folder', (done) => { + const error = 'someError'; + sinon.stub(CodeBuilder.prototype, '_selectBuildFlowClass'); + sinon.stub(CodeBuilder.prototype, '_setUpBuildFolder').throws(new Error(error)); + const codeBuilder = new CodeBuilder(config); - it('| shell execute succeeds on win32, expect no error', (done) => { - // setup - codeBuilder.osType = 'WINDOWS'; - codeBuilder.doDebug = true; - sinon.stub(fs, 'ensureDirSync'); - sinon.stub(fs, 'emptyDirSync'); - sinon.stub(fs, 'copySync'); - sinon.stub(shell, 'exec').callsArgWith(2, 0, null); - // call - codeBuilder.execute((err) => { - // verify - expect(shell.exec.args[0][0]).equal(`PowerShell.exe -Command "& {& '${TEST_PACKAGE_BUILDER}' '${TEST_BUILD_FILE}' $True }"`); - expect(err).equal(undefined); - done(); - }); - }); - - it('| shell execute succeeds on non-win32, expect no error', (done) => { - // setup - codeBuilder.osType = 'UNIX'; - codeBuilder.doDebug = true; - sinon.stub(fs, 'ensureDirSync'); - sinon.stub(fs, 'emptyDirSync'); - sinon.stub(fs, 'copySync'); - sinon.stub(shell, 'exec').callsArgWith(2, 0, null); - // call - codeBuilder.execute((err) => { - // verify - expect(shell.exec.args[0][0]).equal(`${TEST_PACKAGE_BUILDER} "${TEST_BUILD_FILE}" true`); - expect(err).equal(undefined); - done(); - }); - }); - - it('| build flow is infered as zip-only, zip error happens, expect error called back', (done) => { - // setup - codeBuilder.osType = 'UNIX'; - codeBuilder.buildFlow = CodeBuilder.ZIP_ONLY_BUILD_FLOW; - sinon.stub(fs, 'ensureDirSync'); - sinon.stub(fs, 'emptyDirSync'); - sinon.stub(fs, 'copySync'); - sinon.stub(zipUtils, 'createTempZip').callsArgWith(1, 'error'); - // call - codeBuilder.execute((err) => { - // verify - expect(err).equal('error'); + codeBuilder.execute((err, res) => { + expect(err.message).eql(error); + expect(res).eql(undefined); done(); }); }); + }); - it('| build flow is infered as zip-only, zip succeeds, expect no error called back', (done) => { - // setup - codeBuilder.osType = 'UNIX'; - codeBuilder.buildFlow = CodeBuilder.ZIP_ONLY_BUILD_FLOW; - sinon.stub(fs, 'ensureDirSync'); - sinon.stub(fs, 'emptyDirSync'); - sinon.stub(fs, 'copySync'); - sinon.stub(zipUtils, 'createTempZip').callsArgWith(1, null, TEST_ZIP_FILE_PATH); - sinon.stub(fs, 'moveSync'); - // call - codeBuilder.execute((err) => { - // verify - expect(err).equal(undefined); - expect(fs.moveSync.args[0][0]).equal(TEST_ZIP_FILE_PATH); - expect(fs.moveSync.args[0][1]).equal(TEST_BUILD_FILE); - done(); - }); - }); + afterEach(() => { + sinon.restore(); }); }); diff --git a/test/unit/controller/skill-code-controller-test.js b/test/unit/controller/skill-code-controller-test.js index 81e8601f..5b1e1677 100644 --- a/test/unit/controller/skill-code-controller-test.js +++ b/test/unit/controller/skill-code-controller-test.js @@ -61,7 +61,7 @@ describe('Controller test - skill code controller test', () => { it('| skillCodeController resolve unique code settings passes but code builder execute fails, expect error throws', (done) => { // setup sinon.stub(SkillCodeController.prototype, '_resolveUniqueCodeList').returns(TEST_CODE_LIST); - sinon.stub(CodeBuilder.prototype, '_decidePackageBuilder'); + sinon.stub(CodeBuilder.prototype, '_selectBuildFlowClass'); sinon.stub(CodeBuilder.prototype, 'execute').callsArgWith(0, 'error'); // call skillCodeController.buildCode((err) => { @@ -74,7 +74,7 @@ describe('Controller test - skill code controller test', () => { it('| skillCodeController resolve unique code settings and code builder execute pass, expect no error called back', (done) => { // setup sinon.stub(SkillCodeController.prototype, '_resolveUniqueCodeList').returns(TEST_CODE_LIST); - sinon.stub(CodeBuilder.prototype, '_decidePackageBuilder'); + sinon.stub(CodeBuilder.prototype, '_selectBuildFlowClass'); sinon.stub(CodeBuilder.prototype, 'execute').callsArgWith(0); // call skillCodeController.buildCode((err) => { diff --git a/test/unit/fixture/build-flow/some-file.txt b/test/unit/fixture/build-flow/some-file.txt new file mode 100644 index 00000000..7ffa24e2 --- /dev/null +++ b/test/unit/fixture/build-flow/some-file.txt @@ -0,0 +1 @@ +some-file \ No newline at end of file diff --git a/test/unit/run-test.js b/test/unit/run-test.js index 6cf1bd04..336e2389 100644 --- a/test/unit/run-test.js +++ b/test/unit/run-test.js @@ -14,6 +14,12 @@ process.env.ASK_SHARE_USAGE = false; '@test/unit/builtins/lambda-deployer/helper-test.js', '@test/unit/builtins/cfn-deployer/index-test.js', '@test/unit/builtins/cfn-deployer/helper-test.js', + '@test/unit/builtins/build-flows/abstract-build-flow-test.js', + '@test/unit/builtins/build-flows/custom-test.js', + '@test/unit/builtins/build-flows/java-mvn-test.js', + '@test/unit/builtins/build-flows/nodejs-npm-test.js', + '@test/unit/builtins/build-flows/python-pip-test.js', + '@test/unit/builtins/build-flows/zip-only-test.js', // commands '@test/unit/commands/option-validator-test', '@test/unit/commands/abstract-command-test',