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/workflows/__init__.py b/aws_lambda_builders/workflows/__init__.py index 1a3ed60ec..6a64f6813 100644 --- a/aws_lambda_builders/workflows/__init__.py +++ b/aws_lambda_builders/workflows/__init__.py @@ -8,3 +8,4 @@ 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.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/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/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)