diff --git a/.appveyor.yml b/.appveyor.yml index f526a3c1d..e2a78b322 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -30,7 +30,7 @@ install: # setup go - rmdir c:\go /s /q -- "choco install golang" +- "choco install golang --version 1.11.5" - "choco install bzr" - "choco install dep" - setx PATH "C:\go\bin;C:\gopath\bin;C:\Program Files (x86)\Bazaar\;C:\Program Files\Mercurial;%PATH%;" @@ -40,6 +40,9 @@ install: # setup Gradle - "choco install gradle" +# Echo final Path +- "echo %PATH%" + test_script: - "%PYTHON%\\python.exe -m pytest --cov aws_lambda_builders --cov-report term-missing tests/unit tests/functional" - "%PYTHON%\\python.exe -m pytest tests/integration" diff --git a/.gitignore b/.gitignore index 130accaf0..ab4e9c47e 100644 --- a/.gitignore +++ b/.gitignore @@ -360,6 +360,10 @@ GitHub.sublime-settings !.vscode/extensions.json .history +### .NET Build Folders ### +**/bin/ +**/obj/ + ### Windows ### # Windows thumbnail cache files Thumbs.db diff --git a/.travis.yml b/.travis.yml index 039f25023..773b12127 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,6 +25,16 @@ install: - go get -u github.com/golang/dep/cmd/dep + # Install .NET Core 2.1 + - export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 DOTNET_CLI_TELEMETRY_OPTOUT=1 + - if [ "$LINUX" ]; then sudo apt install libunwind8; fi + - wget https://dot.net/v1/dotnet-install.sh -O /tmp/dotnet-install.sh + - chmod +x /tmp/dotnet-install.sh + - /tmp/dotnet-install.sh -v 2.1.504 + - export DOTNET_ROOT=/home/travis/.dotnet + - export PATH=/home/travis/.dotnet:/home/travis/.dotnet/tools:$PATH + - dotnet --info + # Install the code requirements - make init script: diff --git a/aws_lambda_builders/__init__.py b/aws_lambda_builders/__init__.py index 6c140c2f8..5ffd033a3 100644 --- a/aws_lambda_builders/__init__.py +++ b/aws_lambda_builders/__init__.py @@ -1,5 +1,5 @@ """ AWS Lambda Builder Library """ -__version__ = '0.1.0' +__version__ = '0.2.0' RPC_PROTOCOL_VERSION = "0.2" diff --git a/aws_lambda_builders/workflows/__init__.py b/aws_lambda_builders/workflows/__init__.py index 1a3ed60ec..07d0e5939 100644 --- a/aws_lambda_builders/workflows/__init__.py +++ b/aws_lambda_builders/workflows/__init__.py @@ -8,3 +8,5 @@ import aws_lambda_builders.workflows.go_dep import aws_lambda_builders.workflows.go_modules import aws_lambda_builders.workflows.java_gradle +import aws_lambda_builders.workflows.java_maven +import aws_lambda_builders.workflows.dotnet_clipackage diff --git a/aws_lambda_builders/workflows/dotnet_clipackage/DESIGN.md b/aws_lambda_builders/workflows/dotnet_clipackage/DESIGN.md new file mode 100644 index 000000000..34ccb0e3c --- /dev/null +++ b/aws_lambda_builders/workflows/dotnet_clipackage/DESIGN.md @@ -0,0 +1,86 @@ +# .NET Core - Lambda Builder + +### Scope + +To build .NET Core Lambda functions this builder will use the AWS .NET Core Global Tool [Amazon.Lambda.Tools](https://github.com/aws/aws-extensions-for-dotnet-cli#aws-lambda-amazonlambdatools). +This tool has several commands for building and publishing .NET Core Lambda functions. For this integration +the `dotnet lambda package` command will be used to create a zip file that can be deployed to Lambda. + +The builder will install the Amazon.Lambda.Tools Global Tool or update to the latest version before executing +the package command. + +This builder assumes the [.NET Core command-line interface (CLI)](https://docs.microsoft.com/en-us/dotnet/core/tools/?tabs=netcore2x) +is already installed and added to the path environment variable. This is a reasonable requirement as the +.NET Core CLI is a required tool for .NET Core developers to build any .NET Core project. + +The .NET Core CLI handles the validation that the correct version of .NET Core is installed and errors out when there is +not a correct version. + +### Challenges + +#### Output + +The output of `dotnet lambda package` command is a zip archive that consumers can then deploy to Lambda. For SAM build +the expected output is a directory of all of the output files. To make the package command compatible with the SAM build +this builder will direct the package command to output the zip file in the artifacts folder. Once the package command is complete +it expands the zip file and then deletes the zip file. + +#### Parameters + +The package command takes in serveral parameters. Here is the help for the package command. +```bash +> dotnet lambda package --help +Amazon Lambda Tools for .NET Core applications (3.1.2) +Project Home: https://github.com/aws/aws-extensions-for-dotnet-cli, https://github.com/aws/aws-lambda-dotnet + +package: + Command to package a Lambda project into a zip file ready for deployment + + dotnet lambda package [arguments] [options] + Arguments: + The name of the zip file to package the project into + Options: + -c | --configuration Configuration to build with, for example Release or Debug. + -f | --framework Target framework to compile, for example netcoreapp2.1. + --msbuild-parameters Additional msbuild parameters passed to the 'dotnet publish' command. Add quotes around the value if the value contains spaces. + -pl | --project-location The location of the project, if not set the current directory will be assumed. + -cfg | --config-file Configuration file storing default values for command line arguments. + -pcfg | --persist-config-file If true the arguments used for a successful deployment are persisted to a config file. + -o | --output-package The output zip file name + -dvc | --disable-version-check Disable the .NET Core version check. Only for advanced usage. +``` + +Currently **--framework** is the only required parameter which tells the underlying build process what version of .NET Core to build for. + +Parameters can be passed into the package command either by a config file called **aws-lambda-tools-defaults.json** or on +the command line. All .NET Core project templates provided by AWS contain the **aws-lambda-tools-defaults.json** file which has + configuration and framework set. + +If a parameter is set on the command line it will override any values set in the **aws-lambda-tools-defaults.json**. +An alternative config file can be specified with the **--config-file** parameter. + +This builder will forward any options that were provided to it starting with a '-' into the Lambda package command. Forwarding +all parameters to the Lambda package command keeps the builder future compatible with changes to the package command. The package +command does not error out for unknown parameters. + +### Implementation + +The implementation is broken up into 2 steps. The first action is to make sure the Amazon.Lambda.Tools Global Tool +is installed. The second action is to execute the `dotnet lambda package` command. + +#### Step 1: Install Amazon.Lambda.Tools + +The tool is installed by executing the command `dotnet tool install -g Amazon.Lambda.Tools` This will install the +tool from [NuGet](https://www.nuget.org/packages/Amazon.Lambda.Tools/) the .NET package management system. + +To keep the tool updated the command `dotnet tool update -g Amazon.Lambda.Tools` will be executed if the install +command fail because the tool was already installed. + +It is a requirement for Amazon.Lambda.Tools to maintain backwards compatiblity for the package command. This is an +existing requirement for compatiblity with PowerShell Lambda support and the AWS Tools for Visual Studio Team Services. + +#### Step 2: Build the Lambda Deployment bundle + +To create the Lambda deployment bundle the `dotnet lambda package` command is execute in the project directory. This will +create zip file in the artifacts directory. The builder will then expand the zip file into the zip artifacts folder and +delete the zip file. \ No newline at end of file diff --git a/aws_lambda_builders/workflows/dotnet_clipackage/__init__.py b/aws_lambda_builders/workflows/dotnet_clipackage/__init__.py new file mode 100644 index 000000000..0d61b52ae --- /dev/null +++ b/aws_lambda_builders/workflows/dotnet_clipackage/__init__.py @@ -0,0 +1,5 @@ +""" +Builds .NET Core Lambda functions using Amazon.Lambda.Tools Global Tool https://github.com/aws/aws-extensions-for-dotnet-cli#aws-lambda-amazonlambdatools +""" + +from .workflow import DotnetCliPackageWorkflow diff --git a/aws_lambda_builders/workflows/dotnet_clipackage/actions.py b/aws_lambda_builders/workflows/dotnet_clipackage/actions.py new file mode 100644 index 000000000..9b49b6fb4 --- /dev/null +++ b/aws_lambda_builders/workflows/dotnet_clipackage/actions.py @@ -0,0 +1,85 @@ +""" +Actions for Ruby dependency resolution with Bundler +""" + +import os +import logging + +from aws_lambda_builders.actions import BaseAction, Purpose, ActionFailedError +from .utils import OSUtils +from .dotnetcli import DotnetCLIExecutionError + +LOG = logging.getLogger(__name__) + +class GlobalToolInstallAction(BaseAction): + + """ + A Lambda Builder Action which installs the Amazon.Lambda.Tools .NET Core Global Tool + """ + + NAME = 'GlobalToolInstall' + DESCRIPTION = "Install or update the Amazon.Lambda.Tools .NET Core Global Tool." + PURPOSE = Purpose.COMPILE_SOURCE + + def __init__(self, subprocess_dotnet): + super(GlobalToolInstallAction, self).__init__() + self.subprocess_dotnet = subprocess_dotnet + + def execute(self): + try: + LOG.debug("Installing Amazon.Lambda.Tools Global Tool") + self.subprocess_dotnet.run( + ['tool', 'install', '-g', 'Amazon.Lambda.Tools'], + ) + except DotnetCLIExecutionError as ex: + LOG.debug("Error installing probably due to already installed. Attempt to update to latest version.") + try: + self.subprocess_dotnet.run( + ['tool', 'update', '-g', 'Amazon.Lambda.Tools'], + ) + except DotnetCLIExecutionError as ex: + raise ActionFailedError("Error configuring the Amazon.Lambda.Tools .NET Core Global Tool: " + str(ex)) + +class RunPackageAction(BaseAction): + """ + A Lambda Builder Action which builds the .NET Core project using the Amazon.Lambda.Tools .NET Core Global Tool + """ + + NAME = 'RunPackageAction' + DESCRIPTION = "Execute the `dotnet lambda package` command." + PURPOSE = Purpose.COMPILE_SOURCE + + def __init__(self, source_dir, subprocess_dotnet, artifacts_dir, options, os_utils=None): + super(RunPackageAction, self).__init__() + self.source_dir = source_dir + self.subprocess_dotnet = subprocess_dotnet + self.artifacts_dir = artifacts_dir + self.options = options + self.os_utils = os_utils if os_utils else OSUtils() + + def execute(self): + try: + LOG.debug("Running `dotnet lambda package` in %s", self.source_dir) + + zipfilename = os.path.basename(os.path.normpath(self.source_dir)) + ".zip" + zipfullpath = os.path.join(self.artifacts_dir, zipfilename) + + arguments = ['lambda', 'package', '--output-package', zipfullpath] + + if self.options is not None: + for key in self.options: + if str.startswith(key, "-"): + arguments.append(key) + arguments.append(self.options[key]) + + self.subprocess_dotnet.run( + arguments, + cwd=self.source_dir + ) + + # The dotnet lambda package command outputs a zip file for the package. To make this compatible + # with the workflow, unzip the zip file into the artifacts directory and then delete the zip archive. + self.os_utils.expand_zip(zipfullpath, self.artifacts_dir) + + except DotnetCLIExecutionError as ex: + raise ActionFailedError(str(ex)) diff --git a/aws_lambda_builders/workflows/dotnet_clipackage/dotnetcli.py b/aws_lambda_builders/workflows/dotnet_clipackage/dotnetcli.py new file mode 100644 index 000000000..41e2c2d3b --- /dev/null +++ b/aws_lambda_builders/workflows/dotnet_clipackage/dotnetcli.py @@ -0,0 +1,63 @@ +""" +Wrapper around calls to dotent CLI through a subprocess. +""" + +import sys +import logging + +from .utils import OSUtils + +LOG = logging.getLogger(__name__) + +class DotnetCLIExecutionError(Exception): + """ + Exception raised when dotnet CLI fails. + Will encapsulate error output from the command. + """ + + MESSAGE = "Dotnet CLI Failed: {message}" + + def __init__(self, **kwargs): + Exception.__init__(self, self.MESSAGE.format(**kwargs)) + +class SubprocessDotnetCLI(object): + """ + Wrapper around the Dotnet CLI, encapsulating + execution results. + """ + + def __init__(self, dotnet_exe=None, os_utils=None): + self.os_utils = os_utils if os_utils else OSUtils() + if dotnet_exe is None: + if self.os_utils.is_windows(): + dotnet_exe = 'dotnet.exe' + else: + dotnet_exe = 'dotnet' + + self.dotnet_exe = dotnet_exe + + def run(self, args, cwd=None): + if not isinstance(args, list): + raise ValueError('args must be a list') + + if not args: + raise ValueError('requires at least one arg') + + invoke_dotnet = [self.dotnet_exe] + args + + LOG.debug("executing dotnet: %s", invoke_dotnet) + + p = self.os_utils.popen(invoke_dotnet, + stdout=self.os_utils.pipe, + stderr=self.os_utils.pipe, + cwd=cwd) + + out, err = p.communicate() + + # The package command contains lots of useful information on how the package was created and + # information when the package command was not successful. For that reason the output is + # always written to the output to help developers diagnose issues. + LOG.info(out.decode('utf8').strip()) + + if p.returncode != 0: + raise DotnetCLIExecutionError(message=err.decode('utf8').strip()) diff --git a/aws_lambda_builders/workflows/dotnet_clipackage/dotnetcli_resolver.py b/aws_lambda_builders/workflows/dotnet_clipackage/dotnetcli_resolver.py new file mode 100644 index 000000000..af7a7b980 --- /dev/null +++ b/aws_lambda_builders/workflows/dotnet_clipackage/dotnetcli_resolver.py @@ -0,0 +1,26 @@ +""" +Dotnet executable resolution +""" + +from .utils import OSUtils + +class DotnetCliResolver(object): + + def __init__(self, executable_search_paths=None, os_utils=None): + self.binary = 'dotnet' + self.executable_search_paths = executable_search_paths + self.os_utils = os_utils if os_utils else OSUtils() + + @property + def exec_paths(self): + + # look for the windows executable + paths = self.os_utils.which('dotnet.exe', executable_search_paths=self.executable_search_paths) + if not paths: + # fallback to the non windows name without the .exe suffix + paths = self.os_utils.which('dotnet', executable_search_paths=self.executable_search_paths) + + if not paths: + raise ValueError("No dotnet cli executable found!") + + return paths diff --git a/aws_lambda_builders/workflows/dotnet_clipackage/utils.py b/aws_lambda_builders/workflows/dotnet_clipackage/utils.py new file mode 100644 index 000000000..efd344f1a --- /dev/null +++ b/aws_lambda_builders/workflows/dotnet_clipackage/utils.py @@ -0,0 +1,36 @@ +""" +Commonly used utilities +""" + +import os +import platform +import shutil +import subprocess +import zipfile +from aws_lambda_builders.utils import which + + +class OSUtils(object): + """ + Convenience wrapper around common system functions + """ + + def popen(self, command, stdout=None, stderr=None, env=None, cwd=None): + p = subprocess.Popen(command, stdout=stdout, stderr=stderr, env=env, cwd=cwd) + return p + + def is_windows(self): + return platform.system().lower() == 'windows' + + def which(self, executable, executable_search_paths=None): + return which(executable, executable_search_paths=executable_search_paths) + + def expand_zip(self, zipfullpath,destination_dir): + ziparchive = zipfile.ZipFile(zipfullpath, 'r') + ziparchive.extractall(destination_dir) + ziparchive.close() + os.remove(zipfullpath) + + @property + def pipe(self): + return subprocess.PIPE diff --git a/aws_lambda_builders/workflows/dotnet_clipackage/workflow.py b/aws_lambda_builders/workflows/dotnet_clipackage/workflow.py new file mode 100644 index 000000000..895dc46b6 --- /dev/null +++ b/aws_lambda_builders/workflows/dotnet_clipackage/workflow.py @@ -0,0 +1,50 @@ +""" +.NET Core CLI Package Workflow +""" +from aws_lambda_builders.workflow import BaseWorkflow, Capability + +from .actions import GlobalToolInstallAction, RunPackageAction +from .dotnetcli import SubprocessDotnetCLI +from .dotnetcli_resolver import DotnetCliResolver +from .utils import OSUtils + + +class DotnetCliPackageWorkflow(BaseWorkflow): + + """ + A Lambda builder workflow that knows to build and package .NET Core Lambda functions + """ + NAME = "DotnetCliPackageBuilder" + + CAPABILITY = Capability(language="dotnet", + dependency_manager="cli-package", + application_framework=None) + + def __init__(self, + source_dir, + artifacts_dir, + scratch_dir, + manifest_path, + runtime=None, + **kwargs): + + super(DotnetCliPackageWorkflow, self).__init__( + source_dir, + artifacts_dir, + scratch_dir, + manifest_path, + runtime=runtime, + **kwargs) + + options = kwargs["options"] if "options" in kwargs else {} + subprocess_dotnetcli = SubprocessDotnetCLI(os_utils=OSUtils()) + dotnetcli_install = GlobalToolInstallAction(subprocess_dotnet=subprocess_dotnetcli) + + dotnetcli_deployment = RunPackageAction(source_dir, subprocess_dotnet=subprocess_dotnetcli, artifacts_dir=artifacts_dir, options=options) + self.actions = [ + dotnetcli_install, + dotnetcli_deployment, + ] + + def get_resolvers(self): + return [DotnetCliResolver(executable_search_paths=self.executable_search_paths)] diff --git a/aws_lambda_builders/workflows/java_maven/DESIGN.md b/aws_lambda_builders/workflows/java_maven/DESIGN.md new file mode 100644 index 000000000..bf35b3a3b --- /dev/null +++ b/aws_lambda_builders/workflows/java_maven/DESIGN.md @@ -0,0 +1,94 @@ +# Java - Maven Lambda Builder + +## Scope + +This package enables the creation of a Lambda deployment package for Java +projects managed using the Maven build tool. + +## Challenges + +- Java Version compatibility mentioned in the [Gradle Lambda Builder] Design doc. + +- Building Multimodule project (out of scope for the current version) + +Here `ProjectA` is a a single lambda function, and `ProjectB` is a multimodule +project where sub modules `lambda1` and `lambda2` are each a lambda +function. In addition, suppose that `ProjectB/lambda1` has a dependency on its +sibling module `ProjectB/common`. + +**Single-module Project A** +``` +ProjectA +├── pom.xml +├── src +└── template.yaml +``` + +**Multi-module Project B** +``` +ProjectB +├── common +│   └── pom.xml +├── lambda1 +│   └── pom.xml +├── lambda2 +│   └── pom.xml +├── pom.xml +└── template.yaml +``` + +Building Project A is relatively simple since we just need to issue `mvn +package` and place the built classes and dependency jars within the artifact directory. + +Building `ProjectB/lambda1` requires maven to build `lambda1` module from +the root pom directory and use `--also-make` option to build necessary dependencies +(`ProjectB/common` in this case) first before building `ProjectB/lambda1`. This is because +maven is not able to find its way back up to the parent `ProjectB` to +also build `ProjectB/common`. The challenge part here is to find the parent pom directory +especially for the projects with multiple level of submodules. Building multi-module project is +out of scope for the current version. + +## Implementation + +### Build Workflow + +#### Step 1: Copy source project to scratch directory + +By default, Maven stores its build-related metadata in a `target` +directory under the source directory and there is no way to change the output +directory from command line. To avoid writing anything under `source_dir`, +we copy the source project to scratch directory and build it from there. + +#### Step 2: Check Java version and emit warning + +Check whether the local JDK version is <= Java 8, and if it is not, emit a +warning that the built artifact may not run in Lambda unless a) the project is +properly configured (i.e. using `maven.compiler.target`) or b) the project is +built within a Lambda-compatible environment like `lambci`. + +We use Maven to check the actual JVM version Maven is using in case it has been +configured to use a different one than can be found on the PATH. + +#### Step 3: Build and package + +We leverage Maven to do all the heavy lifting for executing the`mvn package` which +will resolve and download the dependencies and build the project. Built java classes +will be located in `target/classes`. Then we use `mvn dependency:copy-dependenceis` to copy +the dependencies and the dependencies will be located in `target/dependency` under the +source directory. + +```bash +MODULE_NAME=$(mvn -q -Dexec.executable=echo -Dexec.args='${project.artifactId}' exec:exec --non-recursive) +mvn clean package -pl :MODULE_NAME -am +mvn dependency:copy-dependencies -DincludeScope=compile -pl :MODULE_NAME +``` + +Here `MODULE_NAME` is the `artifactId` defined in the pom.xml of the project. Maven +will build the dependencies of that project in the reactor and then build the project itself. + +#### Step 4: Copy to artifact directory + +Built Java classes and dependencies are copied from `scratch_dir/target/classes` and `scratch_dir/target/dependency` +to `artifact_dir` and `artifact_dir/lib` respectively. + +[Gradle Lambda Builder]:https://github.com/awslabs/aws-lambda-builders/blob/develop/aws_lambda_builders/workflows/java_gradle/DESIGN.md \ No newline at end of file diff --git a/aws_lambda_builders/workflows/java_maven/__init__.py b/aws_lambda_builders/workflows/java_maven/__init__.py new file mode 100644 index 000000000..775dc9161 --- /dev/null +++ b/aws_lambda_builders/workflows/java_maven/__init__.py @@ -0,0 +1,5 @@ +""" +Builds Java Lambda functions using the Maven build tool +""" + +from .workflow import JavaMavenWorkflow diff --git a/aws_lambda_builders/workflows/java_maven/actions.py b/aws_lambda_builders/workflows/java_maven/actions.py new file mode 100644 index 000000000..a96fb7d03 --- /dev/null +++ b/aws_lambda_builders/workflows/java_maven/actions.py @@ -0,0 +1,106 @@ +""" +Actions for the Java Maven Workflow +""" + +import os +import logging + +from aws_lambda_builders.actions import ActionFailedError, BaseAction, Purpose +from .maven import MavenExecutionError + +LOG = logging.getLogger(__name__) + + +class JavaMavenBaseAction(object): + """ + Base class for Java Maven actions. Provides property of the module name + """ + def __init__(self, + scratch_dir, + subprocess_maven): + self.scratch_dir = scratch_dir + self.subprocess_maven = subprocess_maven + self.artifact_id = None + + @property + def module_name(self): + if self.artifact_id is None: + try: + self.artifact_id = self.subprocess_maven.retrieve_module_name(self.scratch_dir) + except MavenExecutionError as ex: + raise ActionFailedError(str(ex)) + + return self.artifact_id + + +class JavaMavenBuildAction(JavaMavenBaseAction, BaseAction): + NAME = "MavenBuild" + DESCRIPTION = "Building the project using Maven" + PURPOSE = Purpose.COMPILE_SOURCE + + def __init__(self, + scratch_dir, + subprocess_maven): + super(JavaMavenBuildAction, self).__init__(scratch_dir, + subprocess_maven) + self.scratch_dir = scratch_dir + self.subprocess_maven = subprocess_maven + + def execute(self): + try: + self.subprocess_maven.build(self.scratch_dir, + self.module_name) + except MavenExecutionError as ex: + raise ActionFailedError(str(ex)) + + +class JavaMavenCopyDependencyAction(JavaMavenBaseAction, BaseAction): + NAME = "MavenCopyDependency" + DESCRIPTION = "Copy dependency jars to target directory" + PURPOSE = Purpose.COPY_SOURCE + + def __init__(self, + scratch_dir, + subprocess_maven): + super(JavaMavenCopyDependencyAction, self).__init__(scratch_dir, + subprocess_maven) + self.scratch_dir = scratch_dir + self.subprocess_maven = subprocess_maven + + def execute(self): + try: + self.subprocess_maven.copy_dependency(self.scratch_dir, + self.module_name) + except MavenExecutionError as ex: + raise ActionFailedError(str(ex)) + + +class JavaMavenCopyArtifactsAction(BaseAction): + NAME = "MavenCopyArtifacts" + DESCRIPTION = "Copying the built artifacts" + PURPOSE = Purpose.COPY_SOURCE + + def __init__(self, + scratch_dir, + artifacts_dir, + os_utils): + self.scratch_dir = scratch_dir + self.artifacts_dir = artifacts_dir + self.os_utils = os_utils + + def execute(self): + self._copy_artifacts() + + def _copy_artifacts(self): + lambda_build_output = os.path.join(self.scratch_dir, 'target', 'classes') + dependency_output = os.path.join(self.scratch_dir, 'target', 'dependency') + + if not self.os_utils.exists(lambda_build_output): + raise ActionFailedError("Required target/classes directory was not produced from 'mvn package'") + + try: + self.os_utils.copytree(lambda_build_output, self.artifacts_dir) + if self.os_utils.exists(dependency_output): + self.os_utils.copytree(dependency_output, os.path.join(self.artifacts_dir, 'lib')) + except Exception as ex: + raise ActionFailedError(str(ex)) diff --git a/aws_lambda_builders/workflows/java_maven/maven.py b/aws_lambda_builders/workflows/java_maven/maven.py new file mode 100644 index 000000000..f17ab0591 --- /dev/null +++ b/aws_lambda_builders/workflows/java_maven/maven.py @@ -0,0 +1,55 @@ +""" +Wrapper around calls to Maven through a subprocess. +""" + +import logging +import subprocess + +LOG = logging.getLogger(__name__) + + +class MavenExecutionError(Exception): + MESSAGE = "Maven Failed: {message}" + + def __init__(self, **kwargs): + Exception.__init__(self, self.MESSAGE.format(**kwargs)) + + +class SubprocessMaven(object): + + def __init__(self, maven_binary, os_utils=None): + if maven_binary is None: + raise ValueError("Must provide Maven BinaryPath") + self.maven_binary = maven_binary + if os_utils is None: + raise ValueError("Must provide OSUtils") + self.os_utils = os_utils + + def retrieve_module_name(self, scratch_dir): + args = ['-q', '-Dexec.executable=echo', '-Dexec.args=${project.artifactId}', + 'exec:exec', '--non-recursive'] + ret_code, stdout, stderr = self._run(args, scratch_dir) + if ret_code != 0: + raise MavenExecutionError(message=stderr.decode('utf8').strip()) + return stdout.decode('utf8').strip() + + def build(self, scratch_dir, module_name): + args = ['clean', 'install', '-pl', ':' + module_name, '-am'] + ret_code, stdout, stderr = self._run(args, scratch_dir) + + LOG.debug("Maven logs: %s", stdout.decode('utf8').strip()) + + if ret_code != 0: + raise MavenExecutionError(message=stderr.decode('utf8').strip()) + + def copy_dependency(self, scratch_dir, module_name): + args = ['dependency:copy-dependencies', '-DincludeScope=compile', '-pl', ':' + module_name] + ret_code, _, stderr = self._run(args, scratch_dir) + if ret_code != 0: + raise MavenExecutionError(message=stderr.decode('utf8').strip()) + + def _run(self, args, cwd=None): + p = self.os_utils.popen([self.maven_binary.binary_path] + args, cwd=cwd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = p.communicate() + return p.returncode, stdout, stderr diff --git a/aws_lambda_builders/workflows/java_maven/maven_resolver.py b/aws_lambda_builders/workflows/java_maven/maven_resolver.py new file mode 100644 index 000000000..fdb42a9fe --- /dev/null +++ b/aws_lambda_builders/workflows/java_maven/maven_resolver.py @@ -0,0 +1,23 @@ +""" +Maven executable resolution +""" + +from .utils import OSUtils + + +class MavenResolver(object): + + def __init__(self, executable_search_paths=None, os_utils=None): + self.binary = 'mvn' + self.executables = [self.binary] + self.executable_search_paths = executable_search_paths + self.os_utils = os_utils if os_utils else OSUtils() + + @property + def exec_paths(self): + paths = self.os_utils.which('mvn', executable_search_paths=self.executable_search_paths) + + if not paths: + raise ValueError("No Maven executable found!") + + return paths diff --git a/aws_lambda_builders/workflows/java_maven/maven_validator.py b/aws_lambda_builders/workflows/java_maven/maven_validator.py new file mode 100644 index 000000000..783d9dc8f --- /dev/null +++ b/aws_lambda_builders/workflows/java_maven/maven_validator.py @@ -0,0 +1,64 @@ +""" +Maven Binary Validation +""" + +import logging +import re + +from .utils import OSUtils + +LOG = logging.getLogger(__name__) + + +class MavenValidator(object): + VERSION_STRING_WARNING = "%s failed to return a version string using the '-v' option. The workflow is unable to " \ + "check that the version of the JVM used is compatible with AWS Lambda." + + MAJOR_VERSION_WARNING = "%s is using a JVM with major version %s which is newer than 8 that is supported by AWS " \ + "Lambda. The compiled function code may not run in AWS Lambda unless the project has " \ + "been configured to be compatible with Java 8 using 'maven.compiler.target' in Maven." + + def __init__(self, os_utils=None, log=None): + self.language = 'java' + self._valid_binary_path = None + self.os_utils = OSUtils() if not os_utils else os_utils + self.log = LOG if not log else log + + def validate(self, maven_path): + jvm_mv = self._get_major_version(maven_path) + + if jvm_mv: + if int(jvm_mv) > 8: + self.log.warning(self.MAJOR_VERSION_WARNING, maven_path, jvm_mv) + else: + self.log.warning(self.VERSION_STRING_WARNING, maven_path) + + self._valid_binary_path = maven_path + return self._valid_binary_path + + @property + def validated_binary_path(self): + return self._valid_binary_path + + def _get_major_version(self, maven_path): + vs = self._get_jvm_string(maven_path) + if vs: + m = re.search(r'Java version:\s+(\d.*)', vs) + version = m.group(1).split('.') + # For Java 8 or earlier, version strings begin with 1.{Major Version} + if version[0] == '1': + return version[1] + # Starting with Java 9, the major version is first + return version[0] + + def _get_jvm_string(self, maven_path): + p = self.os_utils.popen([maven_path, '-version'], stdout=self.os_utils.pipe, stderr=self.os_utils.pipe) + stdout, _ = p.communicate() + + if p.returncode != 0: + return None + + for l in stdout.splitlines(): + l_dec = l.decode() + if l_dec.startswith('Java version'): + return l_dec diff --git a/aws_lambda_builders/workflows/java_maven/utils.py b/aws_lambda_builders/workflows/java_maven/utils.py new file mode 100644 index 000000000..2e178a320 --- /dev/null +++ b/aws_lambda_builders/workflows/java_maven/utils.py @@ -0,0 +1,54 @@ +""" +Commonly used utilities +""" + +import os +import platform +import shutil +import subprocess +from aws_lambda_builders.utils import which + + +class OSUtils(object): + """ + Convenience wrapper around common system functions + """ + + def popen(self, command, stdout=None, stderr=None, env=None, cwd=None): + p = subprocess.Popen(command, stdout=stdout, stderr=stderr, env=env, cwd=cwd) + return p + + def is_windows(self): + return platform.system().lower() == 'windows' + + def copy(self, src, dst): + shutil.copy2(src, dst) + return dst + + def listdir(self, d): + return os.listdir(d) + + def exists(self, p): + return os.path.exists(p) + + def which(self, executable, executable_search_paths=None): + return which(executable, executable_search_paths=executable_search_paths) + + def copytree(self, source, destination): + if not os.path.exists(destination): + self.makedirs(destination) + names = self.listdir(source) + for name in names: + new_source = os.path.join(source, name) + new_destination = os.path.join(destination, name) + if os.path.isdir(new_source): + self.copytree(new_source, new_destination) + else: + self.copy(new_source, new_destination) + + def makedirs(self, d): + return os.makedirs(d) + + @property + def pipe(self): + return subprocess.PIPE diff --git a/aws_lambda_builders/workflows/java_maven/workflow.py b/aws_lambda_builders/workflows/java_maven/workflow.py new file mode 100644 index 000000000..9960fb938 --- /dev/null +++ b/aws_lambda_builders/workflows/java_maven/workflow.py @@ -0,0 +1,58 @@ +""" +Java Maven Workflow +""" +from aws_lambda_builders.workflow import BaseWorkflow, Capability +from aws_lambda_builders.actions import CopySourceAction +from .actions import JavaMavenBuildAction, JavaMavenCopyDependencyAction, JavaMavenCopyArtifactsAction +from .maven import SubprocessMaven +from .maven_resolver import MavenResolver +from .maven_validator import MavenValidator +from .utils import OSUtils + + +class JavaMavenWorkflow(BaseWorkflow): + """ + A Lambda builder workflow that knows how to build Java projects using Maven. + """ + NAME = "JavaMavenWorkflow" + + CAPABILITY = Capability(language="java", + dependency_manager="maven", + application_framework=None) + + EXCLUDED_FILES = (".aws-sam") + + def __init__(self, + source_dir, + artifacts_dir, + scratch_dir, + manifest_path, + **kwargs): + super(JavaMavenWorkflow, self).__init__(source_dir, + artifacts_dir, + scratch_dir, + manifest_path, + **kwargs) + + self.os_utils = OSUtils() + # Assuming root_dir is the same as source_dir for now + root_dir = source_dir + subprocess_maven = SubprocessMaven(maven_binary=self.binaries['mvn'], os_utils=self.os_utils) + + self.actions = [ + CopySourceAction(root_dir, scratch_dir, excludes=self.EXCLUDED_FILES), + + JavaMavenBuildAction(scratch_dir, + subprocess_maven), + JavaMavenCopyDependencyAction(scratch_dir, + subprocess_maven), + JavaMavenCopyArtifactsAction(scratch_dir, + artifacts_dir, + self.os_utils) + ] + + def get_resolvers(self): + return [MavenResolver(executable_search_paths=self.executable_search_paths)] + + def get_validators(self): + return [MavenValidator(self.os_utils)] diff --git a/aws_lambda_builders/workflows/python_pip/packager.py b/aws_lambda_builders/workflows/python_pip/packager.py index 9ba1dcaa9..a89ae626e 100644 --- a/aws_lambda_builders/workflows/python_pip/packager.py +++ b/aws_lambda_builders/workflows/python_pip/packager.py @@ -631,8 +631,8 @@ def download_all_dependencies(self, requirements_filename, directory): raise NoSuchPackageError(str(package_name)) raise PackageDownloadError(error) stdout = out.decode() - match = re.search(self._LINK_IS_DIR_PATTERN, stdout) - if match: + matches = re.finditer(self._LINK_IS_DIR_PATTERN, stdout) + for match in matches: wheel_package_path = str(match.group(1)) # Looks odd we do not check on the error status of building the # wheel here. We can assume this is a valid package path since diff --git a/setup.py b/setup.py index 7dcd2d304..ecbf3d7e1 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ def read_version(): author_email='aws-sam-developers@amazon.com', url='https://github.com/awslabs/aws-lambda-builders', license='Apache License 2.0', - packages=find_packages(exclude=('tests', 'docs')), + packages=find_packages(exclude=['tests.*', 'tests']), keywords="AWS Lambda Functions Building", # Support Python 2.7 and 3.6 or greater python_requires=( diff --git a/tests/functional/workflows/java_maven/test_java_utils.py b/tests/functional/workflows/java_maven/test_java_utils.py new file mode 100644 index 000000000..621e55e68 --- /dev/null +++ b/tests/functional/workflows/java_maven/test_java_utils.py @@ -0,0 +1,57 @@ +import os +import sys +import tempfile + +from unittest import TestCase + +from aws_lambda_builders.workflows.java_maven import utils + + +class TestOSUtils(TestCase): + + def setUp(self): + self.src = tempfile.mkdtemp() + self.dst = tempfile.mkdtemp() + self.os_utils = utils.OSUtils() + + def test_popen_runs_a_process_and_returns_outcome(self): + cwd_py = os.path.join(os.path.dirname(__file__), '..', '..', 'testdata', 'cwd.py') + p = self.os_utils.popen([sys.executable, cwd_py], + stdout=self.os_utils.pipe, + stderr=self.os_utils.pipe) + out, err = p.communicate() + self.assertEqual(p.returncode, 0) + self.assertEqual(out.decode('utf8').strip(), os.getcwd()) + + def test_popen_can_accept_cwd(self): + testdata_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'testdata') + p = self.os_utils.popen([sys.executable, 'cwd.py'], + stdout=self.os_utils.pipe, + stderr=self.os_utils.pipe, + cwd=testdata_dir) + out, err = p.communicate() + self.assertEqual(p.returncode, 0) + self.assertEqual(out.decode('utf8').strip(), os.path.abspath(testdata_dir)) + + def test_listdir(self): + names = ['a', 'b', 'c'] + for n in names: + self.new_file(self.src, n) + self.assertEquals(set(names), set(self.os_utils.listdir(self.src))) + + def test_copy(self): + f = self.new_file(self.src, 'a') + expected = os.path.join(self.dst, 'a') + copy_ret = self.os_utils.copy(f, expected) + self.assertEquals(expected, copy_ret) + self.assertTrue('a' in os.listdir(self.dst)) + + def test_exists(self): + self.new_file(self.src, 'foo') + self.assertTrue(self.os_utils.exists(os.path.join(self.src, 'foo'))) + + def new_file(self, d, name): + p = os.path.join(d, name) + with open(p, 'w') as f: + f.close() + return p diff --git a/tests/integration/workflows/dotnet_clipackage/test_dotnet.py b/tests/integration/workflows/dotnet_clipackage/test_dotnet.py new file mode 100644 index 000000000..2e1ec20de --- /dev/null +++ b/tests/integration/workflows/dotnet_clipackage/test_dotnet.py @@ -0,0 +1,65 @@ +import os +import shutil +import tempfile + + +from unittest import TestCase + +from aws_lambda_builders.builder import LambdaBuilder + + +class TestDotnetDep(TestCase): + TEST_DATA_FOLDER = os.path.join(os.path.dirname(__file__), "testdata") + + def setUp(self): + self.artifacts_dir = tempfile.mkdtemp() + self.scratch_dir = tempfile.mkdtemp() + + self.builder = LambdaBuilder(language="dotnet", + dependency_manager="cli-package", + application_framework=None) + + self.runtime = "dotnetcore2.1" + + def tearDown(self): + shutil.rmtree(self.artifacts_dir) + shutil.rmtree(self.scratch_dir) + + def test_with_defaults_file(self): + source_dir = os.path.join(self.TEST_DATA_FOLDER, "WithDefaultsFile") + + self.builder.build(source_dir, self.artifacts_dir, self.scratch_dir, + source_dir, + runtime=self.runtime) + + expected_files = {"Amazon.Lambda.Core.dll", + "Amazon.Lambda.Serialization.Json.dll", + "Newtonsoft.Json.dll", + "WithDefaultsFile.deps.json", + "WithDefaultsFile.dll", + "WithDefaultsFile.pdb", + "WithDefaultsFile.runtimeconfig.json"} + + output_files = set(os.listdir(self.artifacts_dir)) + + self.assertEquals(expected_files, output_files) + + def test_require_parameters(self): + source_dir = os.path.join(self.TEST_DATA_FOLDER, "RequireParameters") + + self.builder.build(source_dir, self.artifacts_dir, self.scratch_dir, + source_dir, + runtime=self.runtime, + options={"--framework": "netcoreapp2.1", "--configuration": "Debug"}) + + expected_files = {"Amazon.Lambda.Core.dll", + "Amazon.Lambda.Serialization.Json.dll", + "Newtonsoft.Json.dll", + "RequireParameters.deps.json", + "RequireParameters.dll", + "RequireParameters.pdb", + "RequireParameters.runtimeconfig.json"} + + output_files = set(os.listdir(self.artifacts_dir)) + + self.assertEquals(expected_files, output_files) diff --git a/tests/integration/workflows/dotnet_clipackage/testdata/RequireParameters/RequireParameters.cs b/tests/integration/workflows/dotnet_clipackage/testdata/RequireParameters/RequireParameters.cs new file mode 100644 index 000000000..602632404 --- /dev/null +++ b/tests/integration/workflows/dotnet_clipackage/testdata/RequireParameters/RequireParameters.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Amazon.Lambda.Core; + +// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. +[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))] + +namespace RequireParameters +{ + public class Function + { + + /// + /// A simple function that takes a string and does a ToUpper + /// + /// + /// + /// + public string FunctionHandler(string input, ILambdaContext context) + { + return input?.ToUpper(); + } + } +} diff --git a/tests/integration/workflows/dotnet_clipackage/testdata/RequireParameters/RequireParameters.csproj b/tests/integration/workflows/dotnet_clipackage/testdata/RequireParameters/RequireParameters.csproj new file mode 100644 index 000000000..45c85fd29 --- /dev/null +++ b/tests/integration/workflows/dotnet_clipackage/testdata/RequireParameters/RequireParameters.csproj @@ -0,0 +1,11 @@ + + + netcoreapp2.1 + true + Lambda + + + + + + \ No newline at end of file diff --git a/tests/integration/workflows/dotnet_clipackage/testdata/WithDefaultsFile/Function.cs b/tests/integration/workflows/dotnet_clipackage/testdata/WithDefaultsFile/Function.cs new file mode 100644 index 000000000..23fc86994 --- /dev/null +++ b/tests/integration/workflows/dotnet_clipackage/testdata/WithDefaultsFile/Function.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Amazon.Lambda.Core; + +// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. +[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))] + +namespace WithDefaultsFile +{ + public class Function + { + + /// + /// A simple function that takes a string and does a ToUpper + /// + /// + /// + /// + public string FunctionHandler(string input, ILambdaContext context) + { + return input?.ToUpper(); + } + } +} diff --git a/tests/integration/workflows/dotnet_clipackage/testdata/WithDefaultsFile/WithDefaultsFile.csproj b/tests/integration/workflows/dotnet_clipackage/testdata/WithDefaultsFile/WithDefaultsFile.csproj new file mode 100644 index 000000000..45c85fd29 --- /dev/null +++ b/tests/integration/workflows/dotnet_clipackage/testdata/WithDefaultsFile/WithDefaultsFile.csproj @@ -0,0 +1,11 @@ + + + netcoreapp2.1 + true + Lambda + + + + + + \ No newline at end of file diff --git a/tests/integration/workflows/dotnet_clipackage/testdata/WithDefaultsFile/aws-lambda-tools-defaults.json b/tests/integration/workflows/dotnet_clipackage/testdata/WithDefaultsFile/aws-lambda-tools-defaults.json new file mode 100644 index 000000000..862ac189b --- /dev/null +++ b/tests/integration/workflows/dotnet_clipackage/testdata/WithDefaultsFile/aws-lambda-tools-defaults.json @@ -0,0 +1,19 @@ +{ + "Information" : [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + + "dotnet lambda help", + + "All the command line options for the Lambda command can be specified in this file." + ], + + "profile":"", + "region" : "", + "configuration" : "Release", + "framework" : "netcoreapp2.1", + "function-runtime":"dotnetcore2.1", + "function-memory-size" : 256, + "function-timeout" : 30, + "function-handler" : "WithDefaultsFile::WithDefaultsFile.Function::FunctionHandler" +} diff --git a/tests/integration/workflows/java_maven/test_java_maven.py b/tests/integration/workflows/java_maven/test_java_maven.py new file mode 100644 index 000000000..66d2fca7e --- /dev/null +++ b/tests/integration/workflows/java_maven/test_java_maven.py @@ -0,0 +1,66 @@ +import os +import shutil +import tempfile + +from unittest import TestCase + +from aws_lambda_builders.builder import LambdaBuilder +from aws_lambda_builders.exceptions import WorkflowFailedError + + +class TestJavaMaven(TestCase): + SINGLE_BUILD_TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), "testdata", "single-build") + + def setUp(self): + self.artifacts_dir = tempfile.mkdtemp() + self.scratch_dir = tempfile.mkdtemp() + self.builder = LambdaBuilder(language='java', dependency_manager='maven', application_framework=None) + self.runtime = 'java' + + def tearDown(self): + shutil.rmtree(self.artifacts_dir) + shutil.rmtree(self.scratch_dir) + + def test_build_single_build_with_deps_resources_exclude_test_jars(self): + source_dir = os.path.join(self.SINGLE_BUILD_TEST_DATA_DIR, 'with-deps') + manifest_path = os.path.join(source_dir, 'pom.xml') + self.builder.build(source_dir, self.artifacts_dir, self.scratch_dir, manifest_path, runtime=self.runtime) + expected_files = [p('aws', 'lambdabuilders', 'Main.class'), p('some_data.txt'), + p('lib', 'annotations-2.1.0.jar')] + self.assert_artifact_contains_files(expected_files) + self.assert_artifact_not_contains_file(p('lib', 'junit-4.12.jar')) + self.assert_src_dir_not_touched(source_dir) + + def test_build_single_build_no_deps(self): + source_dir = os.path.join(self.SINGLE_BUILD_TEST_DATA_DIR, 'no-deps') + manifest_path = os.path.join(source_dir, 'pom.xml') + self.builder.build(source_dir, self.artifacts_dir, self.scratch_dir, manifest_path, runtime=self.runtime) + expected_files = [p('aws', 'lambdabuilders', 'Main.class'), p('some_data.txt')] + self.assert_artifact_contains_files(expected_files) + self.assert_artifact_not_contains_file(p('lib')) + self.assert_src_dir_not_touched(source_dir) + + def test_build_single_build_with_deps_broken(self): + source_dir = os.path.join(self.SINGLE_BUILD_TEST_DATA_DIR, 'with-deps-broken') + manifest_path = os.path.join(source_dir, 'pom.xml') + with self.assertRaises(WorkflowFailedError) as raised: + self.builder.build(source_dir, self.artifacts_dir, self.scratch_dir, manifest_path, runtime=self.runtime) + self.assertTrue(raised.exception.args[0].startswith('JavaMavenWorkflow:MavenBuild - Maven Failed')) + self.assert_src_dir_not_touched(source_dir) + + def assert_artifact_contains_files(self, files): + for f in files: + self.assert_artifact_contains_file(f) + + def assert_artifact_contains_file(self, p): + self.assertTrue(os.path.exists(os.path.join(self.artifacts_dir, p))) + + def assert_artifact_not_contains_file(self, p): + self.assertFalse(os.path.exists(os.path.join(self.artifacts_dir, p))) + + def assert_src_dir_not_touched(self, source_dir): + self.assertFalse(os.path.exists(os.path.join(source_dir, 'target'))) + + +def p(path, *comps): + return os.path.join(path, *comps) diff --git a/tests/integration/workflows/java_maven/testdata/single-build/no-deps/pom.xml b/tests/integration/workflows/java_maven/testdata/single-build/no-deps/pom.xml new file mode 100644 index 000000000..2e01666d3 --- /dev/null +++ b/tests/integration/workflows/java_maven/testdata/single-build/no-deps/pom.xml @@ -0,0 +1,13 @@ + + 4.0.0 + helloworld + HelloWorld + 1.0 + jar + A sample Hello World created for SAM CLI. + + 1.8 + 1.8 + + diff --git a/tests/integration/workflows/java_maven/testdata/single-build/no-deps/src/main/java/aws/lambdabuilders/Main.java b/tests/integration/workflows/java_maven/testdata/single-build/no-deps/src/main/java/aws/lambdabuilders/Main.java new file mode 100644 index 000000000..22c7a289d --- /dev/null +++ b/tests/integration/workflows/java_maven/testdata/single-build/no-deps/src/main/java/aws/lambdabuilders/Main.java @@ -0,0 +1,7 @@ +package aws.lambdabuilders; + +public class Main { + public static void main(String[] args) { + System.out.println("Hello AWS Lambda Builders!"); + } +} diff --git a/tests/integration/workflows/java_maven/testdata/single-build/no-deps/src/main/resources/some_data.txt b/tests/integration/workflows/java_maven/testdata/single-build/no-deps/src/main/resources/some_data.txt new file mode 100644 index 000000000..95d09f2b1 --- /dev/null +++ b/tests/integration/workflows/java_maven/testdata/single-build/no-deps/src/main/resources/some_data.txt @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/tests/integration/workflows/java_maven/testdata/single-build/with-deps-broken/pom.xml b/tests/integration/workflows/java_maven/testdata/single-build/with-deps-broken/pom.xml new file mode 100644 index 000000000..65ee7444e --- /dev/null +++ b/tests/integration/workflows/java_maven/testdata/single-build/with-deps-broken/pom.xml @@ -0,0 +1,27 @@ + + 4.0.0 + helloworld + HelloWorld + 1.0 + jar + A sample Hello World created for SAM CLI. + + 1.8 + 1.8 + + + + + software.amazon.awssdk + annotations + 2.1.0-helloworld + + + junit + junit + 4.12 + test + + + diff --git a/tests/integration/workflows/java_maven/testdata/single-build/with-deps-broken/src/main/java/aws/lambdabuilders/Main.java b/tests/integration/workflows/java_maven/testdata/single-build/with-deps-broken/src/main/java/aws/lambdabuilders/Main.java new file mode 100644 index 000000000..22c7a289d --- /dev/null +++ b/tests/integration/workflows/java_maven/testdata/single-build/with-deps-broken/src/main/java/aws/lambdabuilders/Main.java @@ -0,0 +1,7 @@ +package aws.lambdabuilders; + +public class Main { + public static void main(String[] args) { + System.out.println("Hello AWS Lambda Builders!"); + } +} diff --git a/tests/integration/workflows/java_maven/testdata/single-build/with-deps/pom.xml b/tests/integration/workflows/java_maven/testdata/single-build/with-deps/pom.xml new file mode 100644 index 000000000..5540e0675 --- /dev/null +++ b/tests/integration/workflows/java_maven/testdata/single-build/with-deps/pom.xml @@ -0,0 +1,27 @@ + + 4.0.0 + helloworld + HelloWorld + 1.0 + jar + A sample Hello World created for SAM CLI. + + 1.8 + 1.8 + + + + + software.amazon.awssdk + annotations + 2.1.0 + + + junit + junit + 4.12 + test + + + diff --git a/tests/integration/workflows/java_maven/testdata/single-build/with-deps/src/main/java/aws/lambdabuilders/Main.java b/tests/integration/workflows/java_maven/testdata/single-build/with-deps/src/main/java/aws/lambdabuilders/Main.java new file mode 100644 index 000000000..22c7a289d --- /dev/null +++ b/tests/integration/workflows/java_maven/testdata/single-build/with-deps/src/main/java/aws/lambdabuilders/Main.java @@ -0,0 +1,7 @@ +package aws.lambdabuilders; + +public class Main { + public static void main(String[] args) { + System.out.println("Hello AWS Lambda Builders!"); + } +} diff --git a/tests/integration/workflows/java_maven/testdata/single-build/with-deps/src/main/resources/some_data.txt b/tests/integration/workflows/java_maven/testdata/single-build/with-deps/src/main/resources/some_data.txt new file mode 100644 index 000000000..95d09f2b1 --- /dev/null +++ b/tests/integration/workflows/java_maven/testdata/single-build/with-deps/src/main/resources/some_data.txt @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/tests/unit/workflows/dotnet_clipackage/__init__.py b/tests/unit/workflows/dotnet_clipackage/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/workflows/dotnet_clipackage/test_actions.py b/tests/unit/workflows/dotnet_clipackage/test_actions.py new file mode 100644 index 000000000..a5e2c32e4 --- /dev/null +++ b/tests/unit/workflows/dotnet_clipackage/test_actions.py @@ -0,0 +1,93 @@ +from unittest import TestCase +from mock import patch +import os +import platform + +from aws_lambda_builders.actions import ActionFailedError +from aws_lambda_builders.workflows.dotnet_clipackage.dotnetcli import DotnetCLIExecutionError +from aws_lambda_builders.workflows.dotnet_clipackage.actions import GlobalToolInstallAction, RunPackageAction + + +class TestGlobalToolInstallAction(TestCase): + + @patch("aws_lambda_builders.workflows.dotnet_clipackage.dotnetcli.SubprocessDotnetCLI") + def setUp(self, MockSubprocessDotnetCLI): + self.subprocess_dotnet = MockSubprocessDotnetCLI.return_value + + def test_global_tool_install(self): + self.subprocess_dotnet.reset_mock() + + action = GlobalToolInstallAction(self.subprocess_dotnet) + action.execute() + self.subprocess_dotnet.run.assert_called_once_with(['tool', 'install', '-g', 'Amazon.Lambda.Tools']) + + def test_global_tool_update(self): + self.subprocess_dotnet.reset_mock() + + self.subprocess_dotnet.run.side_effect = [DotnetCLIExecutionError(message="Already Installed"), None] + action = GlobalToolInstallAction(self.subprocess_dotnet) + action.execute() + self.subprocess_dotnet.run.assert_any_call(['tool', 'install', '-g', 'Amazon.Lambda.Tools']) + self.subprocess_dotnet.run.assert_any_call(['tool', 'update', '-g', 'Amazon.Lambda.Tools']) + + def test_global_tool_update_failed(self): + self.subprocess_dotnet.reset_mock() + + self.subprocess_dotnet.run.side_effect = [DotnetCLIExecutionError(message="Already Installed"), + DotnetCLIExecutionError(message="Updated Failed")] + action = GlobalToolInstallAction(self.subprocess_dotnet) + self.assertRaises(ActionFailedError, action.execute) + + +class TestRunPackageAction(TestCase): + + @patch("aws_lambda_builders.workflows.dotnet_clipackage.dotnetcli.SubprocessDotnetCLI") + @patch("aws_lambda_builders.workflows.dotnet_clipackage.utils.OSUtils") + def setUp(self, MockSubprocessDotnetCLI, MockOSUtils): + self.subprocess_dotnet = MockSubprocessDotnetCLI.return_value + self.os_utils = MockOSUtils + self.source_dir = os.path.join('/source_dir') + self.artifacts_dir = os.path.join('/artifacts_dir') + self.scratch_dir = os.path.join('/scratch_dir') + + def test_build_package(self): + self.subprocess_dotnet.reset_mock() + + options = {} + action = RunPackageAction(self.source_dir, self.subprocess_dotnet, self.artifacts_dir, options, self.os_utils) + + action.execute() + + if platform.system().lower() == 'windows': + zipFilePath = '/artifacts_dir\\source_dir.zip' + else: + zipFilePath = '/artifacts_dir/source_dir.zip' + + self.subprocess_dotnet.run.assert_called_once_with(['lambda', 'package', '--output-package', zipFilePath], + cwd='/source_dir') + + def test_build_package_arguments(self): + self.subprocess_dotnet.reset_mock() + + options = {"--framework": "netcoreapp2.1"} + action = RunPackageAction(self.source_dir, self.subprocess_dotnet, self.artifacts_dir, options, self.os_utils) + + action.execute() + + if platform.system().lower() == 'windows': + zipFilePath = '/artifacts_dir\\source_dir.zip' + else: + zipFilePath = '/artifacts_dir/source_dir.zip' + + self.subprocess_dotnet.run.assert_called_once_with(['lambda', 'package', '--output-package', + zipFilePath, '--framework', 'netcoreapp2.1'], + cwd='/source_dir') + + def test_build_error(self): + self.subprocess_dotnet.reset_mock() + + self.subprocess_dotnet.run.side_effect = DotnetCLIExecutionError(message="Failed Package") + options = {} + action = RunPackageAction(self.source_dir, self.subprocess_dotnet, self.artifacts_dir, options, self.os_utils) + + self.assertRaises(ActionFailedError, action.execute) diff --git a/tests/unit/workflows/dotnet_clipackage/test_dotnetcli.py b/tests/unit/workflows/dotnet_clipackage/test_dotnetcli.py new file mode 100644 index 000000000..45243b7e6 --- /dev/null +++ b/tests/unit/workflows/dotnet_clipackage/test_dotnetcli.py @@ -0,0 +1,72 @@ +from unittest import TestCase +from mock import patch, MagicMock + +from aws_lambda_builders.workflows.dotnet_clipackage.dotnetcli import SubprocessDotnetCLI, DotnetCLIExecutionError + + +class TestSubprocessDotnetCLI(TestCase): + + @patch("aws_lambda_builders.workflows.dotnet_clipackage.utils.OSUtils") + def setUp(self, MockOSUtils): + self.os_utils = MockOSUtils.return_value + + def test_dotnetcli_name_windows(self): + self.os_utils.reset_mock() + self.os_utils.is_windows.return_value = True + + dotnetCli = SubprocessDotnetCLI(os_utils=self.os_utils) + + assert dotnetCli.dotnet_exe == 'dotnet.exe' + + def test_dotnetcli_name_non_windows(self): + self.os_utils.reset_mock() + self.os_utils.is_windows.return_value = False + + dotnetCli = SubprocessDotnetCLI(os_utils=self.os_utils) + + assert dotnetCli.dotnet_exe == 'dotnet' + + def test_invalid_args(self): + self.os_utils.reset_mock() + self.os_utils.is_windows.return_value = True + + dotnetCli = SubprocessDotnetCLI(os_utils=self.os_utils) + + self.assertRaises(ValueError, dotnetCli.run, None) + self.assertRaises(ValueError, dotnetCli.run, []) + + def test_success_exitcode(self): + self.os_utils.reset_mock() + self.os_utils.is_windows.return_value = True + + proc = MagicMock() + mockStdOut = MagicMock() + mockStdErr = MagicMock() + proc.communicate.return_value = (mockStdOut, mockStdErr) + proc.returncode = 0 + + mockStdOut.decode.return_value = "useful info" + mockStdErr.decode.return_value = "useful error" + + self.os_utils.popen.return_value = proc + + dotnetCli = SubprocessDotnetCLI(os_utils=self.os_utils) + dotnetCli.run(["--info"]) + + def test_error_exitcode(self): + self.os_utils.reset_mock() + self.os_utils.is_windows.return_value = True + + proc = MagicMock() + mockStdOut = MagicMock() + mockStdErr = MagicMock() + proc.communicate.return_value = (mockStdOut, mockStdErr) + proc.returncode = -1 + + mockStdOut.decode.return_value = "useful info" + mockStdErr.decode.return_value = "useful error" + + self.os_utils.popen.return_value = proc + + dotnetCli = SubprocessDotnetCLI(os_utils=self.os_utils) + self.assertRaises(DotnetCLIExecutionError, dotnetCli.run, ["--info"]) diff --git a/tests/unit/workflows/dotnet_clipackage/test_dotnetcli_resolver.py b/tests/unit/workflows/dotnet_clipackage/test_dotnetcli_resolver.py new file mode 100644 index 000000000..b824d86f6 --- /dev/null +++ b/tests/unit/workflows/dotnet_clipackage/test_dotnetcli_resolver.py @@ -0,0 +1,40 @@ +from unittest import TestCase +from mock import patch + +from aws_lambda_builders.workflows.dotnet_clipackage.dotnetcli_resolver import DotnetCliResolver + + +class TestDotnetCliResolver(TestCase): + + @patch("aws_lambda_builders.workflows.dotnet_clipackage.utils.OSUtils") + def setUp(self, MockOSUtils): + self.os_utils = MockOSUtils.return_value + + def test_found_windows(self): + self.os_utils.reset_mock() + + self.os_utils.which.side_effect = ["c:/dir/dotnet.exe"] + + resolver = DotnetCliResolver(os_utils=self.os_utils) + found = resolver.exec_paths + + self.assertEqual("c:/dir/dotnet.exe", found) + + def test_found_linux(self): + self.os_utils.reset_mock() + + self.os_utils.which.side_effect = [None, "/usr/dotnet/dotnet"] + + resolver = DotnetCliResolver(os_utils=self.os_utils) + found = resolver.exec_paths + + self.assertEqual("/usr/dotnet/dotnet", found) + + def test_not_found(self): + self.os_utils.reset_mock() + self.os_utils.which.side_effect = [None, None] + resolver = DotnetCliResolver(os_utils=self.os_utils) + self.assertRaises(ValueError, self.exec_path_method_wrapper, resolver) + + def exec_path_method_wrapper(self, resolver): + resolver.exec_paths diff --git a/tests/unit/workflows/dotnet_clipackage/test_workflow.py b/tests/unit/workflows/dotnet_clipackage/test_workflow.py new file mode 100644 index 000000000..6dfc4ccd0 --- /dev/null +++ b/tests/unit/workflows/dotnet_clipackage/test_workflow.py @@ -0,0 +1,14 @@ +from unittest import TestCase + +from aws_lambda_builders.workflows.dotnet_clipackage.workflow import DotnetCliPackageWorkflow +from aws_lambda_builders.workflows.dotnet_clipackage.actions import GlobalToolInstallAction, RunPackageAction + + +class TestDotnetCliPackageWorkflow(TestCase): + + def test_actions(self): + workflow = DotnetCliPackageWorkflow("source_dir", "artifacts_dir", "scratch_dir", "manifest_path") + self.assertEqual(workflow.actions.__len__(), 2) + + self.assertIsInstance(workflow.actions[0], GlobalToolInstallAction) + self.assertIsInstance(workflow.actions[1], RunPackageAction) diff --git a/tests/unit/workflows/java_maven/__init__.py b/tests/unit/workflows/java_maven/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/workflows/java_maven/test_actions.py b/tests/unit/workflows/java_maven/test_actions.py new file mode 100644 index 000000000..a2676eb25 --- /dev/null +++ b/tests/unit/workflows/java_maven/test_actions.py @@ -0,0 +1,114 @@ +from unittest import TestCase +from mock import patch, call +import os + +from aws_lambda_builders.actions import ActionFailedError +from aws_lambda_builders.workflows.java_maven.actions import JavaMavenBuildAction, JavaMavenCopyArtifactsAction, \ + JavaMavenCopyDependencyAction, MavenExecutionError + + +class TestJavaMavenBuildAction(TestCase): + + @patch("aws_lambda_builders.workflows.java_maven.maven.SubprocessMaven") + def setUp(self, MockSubprocessMaven): + self.subprocess_maven = MockSubprocessMaven.return_value + self.scratch_dir = os.path.join('scratch_dir') + self.artifacts_dir = os.path.join('artifacts_dir') + self.module_name = "module" + + def test_calls_maven_build(self): + self.subprocess_maven.retrieve_module_name.side_effect = lambda scratch: self.module_name + action = JavaMavenBuildAction(self.scratch_dir, + self.subprocess_maven) + action.execute() + self.subprocess_maven.build.assert_called_with(self.scratch_dir, self.module_name) + + def test_error_building_project_raises_action_error(self): + self.subprocess_maven.build.side_effect = MavenExecutionError(message='Build failed!') + action = JavaMavenBuildAction(self.scratch_dir, + self.subprocess_maven) + with self.assertRaises(ActionFailedError) as raised: + action.execute() + self.assertEquals(raised.exception.args[0], 'Maven Failed: Build failed!') + + +class TestJavaMavenCopyDependencyAction(TestCase): + + @patch("aws_lambda_builders.workflows.java_maven.maven.SubprocessMaven") + def setUp(self, MockSubprocessMaven): + self.subprocess_maven = MockSubprocessMaven.return_value + self.scratch_dir = os.path.join('scratch_dir') + self.artifacts_dir = os.path.join('artifacts_dir') + self.module_name = 'module_name' + + def test_calls_maven_copy_dependency(self): + self.subprocess_maven.retrieve_module_name.side_effect = lambda scratch: self.module_name + action = JavaMavenCopyDependencyAction(self.scratch_dir, + self.subprocess_maven) + action.execute() + self.subprocess_maven.copy_dependency.assert_called_with(self.scratch_dir, self.module_name) + + def test_error_building_project_raises_action_error(self): + self.subprocess_maven.copy_dependency.side_effect = MavenExecutionError(message='Build failed!') + action = JavaMavenCopyDependencyAction(self.scratch_dir, + self.subprocess_maven) + with self.assertRaises(ActionFailedError) as raised: + action.execute() + self.assertEquals(raised.exception.args[0], 'Maven Failed: Build failed!') + + +class TestJavaMavenCopyArtifactsAction(TestCase): + @patch("aws_lambda_builders.workflows.java_maven.utils.OSUtils") + def setUp(self, MockOSUtils): + self.os_utils = MockOSUtils.return_value + self.os_utils.copy.side_effect = lambda src, dst: dst + self.scratch_dir = "scratch_dir" + self.output_dir = os.path.join(self.scratch_dir, 'target', 'classes') + self.artifacts_dir = os.path.join('artifacts_dir') + + def test_copies_artifacts_no_deps(self): + self.os_utils.exists.return_value = True + self.os_utils.copytree.side_effect = lambda src, dst: None + self.os_utils.copy.side_effect = lambda src, dst: None + + action = JavaMavenCopyArtifactsAction(self.scratch_dir, + self.artifacts_dir, + self.os_utils) + action.execute() + + self.os_utils.copytree.assert_has_calls([ + call(os.path.join(self.scratch_dir, 'target', 'classes'), self.artifacts_dir)]) + + def test_copies_artifacts_with_deps(self): + self.os_utils.exists.return_value = True + self.os_utils.copytree.side_effect = lambda src, dst: None + self.os_utils.copy.side_effect = lambda src, dst: None + os.path.join(self.scratch_dir, 'target', 'dependency') + + action = JavaMavenCopyArtifactsAction(self.scratch_dir, + self.artifacts_dir, + self.os_utils) + action.execute() + self.os_utils.copytree.assert_has_calls([ + call(os.path.join(self.scratch_dir, 'target', 'classes'), self.artifacts_dir), + call(os.path.join(self.scratch_dir, 'target', 'dependency'), os.path.join(self.artifacts_dir, 'lib'))]) + + def test_error_in_artifact_copy_raises_action_error(self): + self.os_utils.exists.return_value = True + self.os_utils.copytree.side_effect = Exception("copy failed!") + action = JavaMavenCopyArtifactsAction(self.scratch_dir, + self.artifacts_dir, + self.os_utils) + with self.assertRaises(ActionFailedError) as raised: + action.execute() + self.assertEquals(raised.exception.args[0], "copy failed!") + + def test_missing_required_target_class_directory_raises_action_error(self): + self.os_utils.exists.return_value = False + action = JavaMavenCopyArtifactsAction(self.scratch_dir, + self.artifacts_dir, + self.os_utils) + with self.assertRaises(ActionFailedError) as raised: + action.execute() + self.assertEquals(raised.exception.args[0], "Required target/classes directory was not " + "produced from 'mvn package'") diff --git a/tests/unit/workflows/java_maven/test_maven.py b/tests/unit/workflows/java_maven/test_maven.py new file mode 100644 index 000000000..4ad74576b --- /dev/null +++ b/tests/unit/workflows/java_maven/test_maven.py @@ -0,0 +1,90 @@ +import subprocess + +from unittest import TestCase +from mock import patch + +from aws_lambda_builders.binary_path import BinaryPath +from aws_lambda_builders.workflows.java_maven.maven import SubprocessMaven, MavenExecutionError + + +class FakePopen: + def __init__(self, out=b'out', err=b'err', retcode=0): + self.out = out + self.err = err + self.returncode = retcode + + def communicate(self): + return self.out, self.err + + def wait(self): + pass + + +class TestSubprocessMaven(TestCase): + + @patch("aws_lambda_builders.workflows.java_gradle.utils.OSUtils") + def setUp(self, MockOSUtils): + self.os_utils = MockOSUtils.return_value + self.os_utils.exists.side_effect = lambda d: True + self.popen = FakePopen() + self.os_utils.popen.side_effect = [self.popen] + self.maven_path = '/path/to/mvn' + self.maven_binary = BinaryPath(None, None, 'mvn', binary_path=self.maven_path) + self.source_dir = '/foo/bar/helloworld' + self.module_name = 'helloworld' + + def test_no_os_utils_build_init_throws(self): + with self.assertRaises(ValueError) as err_assert: + SubprocessMaven(maven_binary=self.maven_binary) + self.assertEquals(err_assert.exception.args[0], 'Must provide OSUtils') + + def test_no_maven_exec_init_throws(self): + with self.assertRaises(ValueError) as err_assert: + SubprocessMaven(None) + self.assertEquals(err_assert.exception.args[0], 'Must provide Maven BinaryPath') + + def test_retrieve_module_name(self): + maven = SubprocessMaven(maven_binary=self.maven_binary, os_utils=self.os_utils) + maven.retrieve_module_name(self.source_dir) + self.os_utils.popen.assert_called_with( + [self.maven_path, '-q', '-Dexec.executable=echo', '-Dexec.args=${project.artifactId}', + 'exec:exec', '--non-recursive'], + cwd=self.source_dir, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + + def test_retrieve_module_name_raises_exception_if_retcode_not_0(self): + self.popen = FakePopen(retcode=1, err=b'Some Error Message') + self.os_utils.popen.side_effect = [self.popen] + maven = SubprocessMaven(maven_binary=self.maven_binary, os_utils=self.os_utils) + with self.assertRaises(MavenExecutionError) as err: + maven.retrieve_module_name(self.source_dir) + self.assertEquals(err.exception.args[0], 'Maven Failed: Some Error Message') + + def test_build_project(self): + maven = SubprocessMaven(maven_binary=self.maven_binary, os_utils=self.os_utils) + maven.build(self.source_dir, self.module_name) + self.os_utils.popen.assert_called_with( + [self.maven_path, 'clean', 'install', '-pl', ':' + self.module_name, '-am'], + cwd=self.source_dir, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + + def test_build_raises_exception_if_retcode_not_0(self): + self.popen = FakePopen(retcode=1, err=b'Some Error Message') + self.os_utils.popen.side_effect = [self.popen] + maven = SubprocessMaven(maven_binary=self.maven_binary, os_utils=self.os_utils) + with self.assertRaises(MavenExecutionError) as err: + maven.build(self.source_dir, self.module_name) + self.assertEquals(err.exception.args[0], 'Maven Failed: Some Error Message') + + def test_copy_dependency(self): + maven = SubprocessMaven(maven_binary=self.maven_binary, os_utils=self.os_utils) + maven.copy_dependency(self.source_dir, self.module_name) + self.os_utils.popen.assert_called_with( + [self.maven_path, 'dependency:copy-dependencies', '-DincludeScope=compile', '-pl', ':' + self.module_name], + cwd=self.source_dir, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + + def test_copy_dependency_raises_exception_if_retcode_not_0(self): + self.popen = FakePopen(retcode=1, err=b'Some Error Message') + self.os_utils.popen.side_effect = [self.popen] + maven = SubprocessMaven(maven_binary=self.maven_binary, os_utils=self.os_utils) + with self.assertRaises(MavenExecutionError) as err: + maven.copy_dependency(self.source_dir, self.module_name) + self.assertEquals(err.exception.args[0], 'Maven Failed: Some Error Message') diff --git a/tests/unit/workflows/java_maven/test_maven_resolver.py b/tests/unit/workflows/java_maven/test_maven_resolver.py new file mode 100644 index 000000000..3e67b7df4 --- /dev/null +++ b/tests/unit/workflows/java_maven/test_maven_resolver.py @@ -0,0 +1,26 @@ +from unittest import TestCase + +from mock import patch +from aws_lambda_builders.workflows.java_maven.maven_resolver import MavenResolver + + +class TestMavenResolver(TestCase): + + @patch("aws_lambda_builders.workflows.java_gradle.utils.OSUtils") + def setUp(self, MockOSUtils): + self.mock_os_utils = MockOSUtils.return_value + self.mock_os_utils.is_windows.side_effect = [False] + + def test_returns_maven_on_path(self): + maven_path = '/path/to/mvn' + self.mock_os_utils.which.side_effect = lambda executable, executable_search_paths: [maven_path] + + resolver = MavenResolver(os_utils=self.mock_os_utils) + self.assertEquals(resolver.exec_paths, [maven_path]) + + def test_throws_value_error_if_no_exec_found(self): + self.mock_os_utils.which.side_effect = lambda executable, executable_search_paths: [] + resolver = MavenResolver(os_utils=self.mock_os_utils) + with self.assertRaises(ValueError) as raised: + resolver.exec_paths() + self.assertEquals(raised.exception.args[0], 'No Maven executable found!') diff --git a/tests/unit/workflows/java_maven/test_maven_validator.py b/tests/unit/workflows/java_maven/test_maven_validator.py new file mode 100644 index 000000000..0fae44764 --- /dev/null +++ b/tests/unit/workflows/java_maven/test_maven_validator.py @@ -0,0 +1,75 @@ +from unittest import TestCase + +from mock import patch, Mock +from parameterized import parameterized +from aws_lambda_builders.workflows.java_maven.maven_validator import MavenValidator + + +class FakePopen(object): + def __init__(self, stdout=None, stderr=None, returncode=0): + self._stdout = stdout + self._stderr = stderr + self._returncode = returncode + + def communicate(self): + return self._stdout, self._stderr + + @property + def returncode(self): + return self._returncode + + +class TestMavenBinaryValidator(TestCase): + + @patch("aws_lambda_builders.workflows.java_gradle.utils.OSUtils") + def setUp(self, MockOSUtils): + self.mock_os_utils = MockOSUtils.return_value + self.mock_log = Mock() + self.maven_path = '/path/to/maven' + + @parameterized.expand([ + '1.7.0', + '1.8.9', + '11.0.0' + ]) + def test_accepts_any_jvm_mv(self, version): + version_string = ('Java version: %s, vendor: Oracle Corporation' % version).encode() + self.mock_os_utils.popen.side_effect = [FakePopen(stdout=version_string)] + validator = MavenValidator(os_utils=self.mock_os_utils) + self.assertTrue(validator.validate(maven_path=self.maven_path)) + self.assertEqual(validator.validated_binary_path, self.maven_path) + + def test_emits_warning_when_jvm_mv_greater_than_8(self): + version_string = 'Java version: 10.0.1, vendor: Oracle Corporation'.encode() + self.mock_os_utils.popen.side_effect = [FakePopen(stdout=version_string)] + validator = MavenValidator(os_utils=self.mock_os_utils, log=self.mock_log) + self.assertTrue(validator.validate(maven_path=self.maven_path)) + self.assertEqual(validator.validated_binary_path, self.maven_path) + self.mock_log.warning.assert_called_with(MavenValidator.MAJOR_VERSION_WARNING, self.maven_path, '10') + + @parameterized.expand([ + '1.6.0', + '1.7.0', + '1.8.9' + ]) + def test_does_not_emit_warning_when_jvm_mv_8_or_less(self, version): + version_string = ('Java version: %s, vendor: Oracle Corporation' % version).encode() + self.mock_os_utils.popen.side_effect = [FakePopen(stdout=version_string)] + validator = MavenValidator(os_utils=self.mock_os_utils, log=self.mock_log) + self.assertTrue(validator.validate(maven_path=self.maven_path)) + self.assertEqual(validator.validated_binary_path, self.maven_path) + self.mock_log.warning.assert_not_called() + + def test_emits_warning_when_maven_excutable_fails(self): + version_string = 'Java version: %s, vendor: Oracle Corporation'.encode() + self.mock_os_utils.popen.side_effect = [FakePopen(stdout=version_string, returncode=1)] + validator = MavenValidator(os_utils=self.mock_os_utils, log=self.mock_log) + validator.validate(maven_path=self.maven_path) + self.mock_log.warning.assert_called_with(MavenValidator.VERSION_STRING_WARNING, self.maven_path) + + def test_emits_warning_when_version_string_not_found(self): + version_string = 'Blah: 9.0.0'.encode() + self.mock_os_utils.popen.side_effect = [FakePopen(stdout=version_string, returncode=0)] + validator = MavenValidator(os_utils=self.mock_os_utils, log=self.mock_log) + validator.validate(maven_path=self.maven_path) + self.mock_log.warning.assert_called_with(MavenValidator.VERSION_STRING_WARNING, self.maven_path) diff --git a/tests/unit/workflows/java_maven/test_workflow.py b/tests/unit/workflows/java_maven/test_workflow.py new file mode 100644 index 000000000..b4f89fbfc --- /dev/null +++ b/tests/unit/workflows/java_maven/test_workflow.py @@ -0,0 +1,44 @@ +from unittest import TestCase + +from aws_lambda_builders.workflows.java_maven.workflow import JavaMavenWorkflow +from aws_lambda_builders.workflows.java_maven.actions import \ + JavaMavenBuildAction, JavaMavenCopyArtifactsAction, JavaMavenCopyDependencyAction +from aws_lambda_builders.actions import CopySourceAction +from aws_lambda_builders.workflows.java_maven.maven_resolver import MavenResolver +from aws_lambda_builders.workflows.java_maven.maven_validator import MavenValidator + + +class TestJavaMavenWorkflow(TestCase): + """ + the workflow requires an external utility (maven) to run, so it is extensively tested in integration tests. + this is just a quick wiring test to provide fast feedback if things are badly broken + """ + + def test_workflow_sets_up_maven_actions(self): + workflow = JavaMavenWorkflow("source", "artifacts", "scratch_dir", "manifest") + + self.assertEqual(len(workflow.actions), 4) + + self.assertIsInstance(workflow.actions[0], CopySourceAction) + + self.assertIsInstance(workflow.actions[1], JavaMavenBuildAction) + + self.assertIsInstance(workflow.actions[2], JavaMavenCopyDependencyAction) + + self.assertIsInstance(workflow.actions[3], JavaMavenCopyArtifactsAction) + + def test_workflow_sets_up_resolvers(self): + workflow = JavaMavenWorkflow("source", "artifacts", "scratch_dir", "manifest") + + resolvers = workflow.get_resolvers() + self.assertEqual(len(resolvers), 1) + + self.assertIsInstance(resolvers[0], MavenResolver) + + def test_workflow_sets_up_validators(self): + workflow = JavaMavenWorkflow("source", "artifacts", "scratch_dir", "manifest") + + validators = workflow.get_validators() + self.assertEqual(len(validators), 1) + + self.assertIsInstance(validators[0], MavenValidator) diff --git a/tests/unit/workflows/python_pip/test_packager.py b/tests/unit/workflows/python_pip/test_packager.py index e1d58dd74..937f52aad 100644 --- a/tests/unit/workflows/python_pip/test_packager.py +++ b/tests/unit/workflows/python_pip/test_packager.py @@ -248,6 +248,36 @@ def test_download_wheels_no_wheels(self, pip_factory): runner.download_manylinux_wheels([], 'directory', "abi") assert len(pip.calls) == 0 + def test_does_find_local_directory(self, pip_factory): + pip, runner = pip_factory() + pip.add_return((0, + (b"Processing ../local-dir\n" + b" Link is a directory," + b" ignoring download_dir"), + b'')) + runner.download_all_dependencies('requirements.txt', 'directory') + assert len(pip.calls) == 2 + assert pip.calls[1].args == ['wheel', '--no-deps', '--wheel-dir', + 'directory', '../local-dir'] + + def test_does_find_multiple_local_directories(self, pip_factory): + pip, runner = pip_factory() + pip.add_return((0, + (b"Processing ../local-dir-1\n" + b" Link is a directory," + b" ignoring download_dir" + b"\nsome pip output...\n" + b"Processing ../local-dir-2\n" + b" Link is a directory," + b" ignoring download_dir"), + b'')) + runner.download_all_dependencies('requirements.txt', 'directory') + assert len(pip.calls) == 3 + assert pip.calls[1].args == ['wheel', '--no-deps', '--wheel-dir', + 'directory', '../local-dir-1'] + assert pip.calls[2].args == ['wheel', '--no-deps', '--wheel-dir', + 'directory', '../local-dir-2'] + def test_raise_no_such_package_error(self, pip_factory): pip, runner = pip_factory() pip.add_return((1, b'',