import os from pathlib import Path from unittest import TestCase from unittest.mock import ANY, patch, call from aws_lambda_builders.actions import ( CopySourceAction, CleanUpAction, MoveDependenciesAction, LinkSourceAction, LinkSinglePathAction, ) from aws_lambda_builders.architecture import ARM64 from aws_lambda_builders.workflows.nodejs_npm.actions import NodejsNpmInstallAction, NodejsNpmCIAction from aws_lambda_builders.workflows.nodejs_npm_esbuild import NodejsNpmEsbuildWorkflow from aws_lambda_builders.workflows.nodejs_npm_esbuild.actions import EsbuildBundleAction from aws_lambda_builders.workflows.nodejs_npm_esbuild.esbuild import SubprocessEsbuild from aws_lambda_builders.workflows.nodejs_npm_esbuild.exceptions import EsbuildExecutionError 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 class TestNodejsNpmEsbuildWorkflow(TestCase): """ the workflow requires an external utility (npm) 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 """ @patch("aws_lambda_builders.workflows.nodejs_npm.utils.OSUtils") def setUp(self, OSUtilMock): self.osutils = OSUtilMock.return_value self.osutils.pipe = "PIPE" self.popen = FakePopen() self.osutils.popen.side_effect = [self.popen] self.osutils.dirname.return_value = "source" self.osutils.is_windows.side_effect = [False] self.osutils.joinpath.side_effect = lambda a, b: "{}/{}".format(a, b) def test_workflow_sets_up_npm_actions_with_bundler_if_manifest_requests_it(self): self.osutils.file_exists.side_effect = [True, False, False] workflow = NodejsNpmEsbuildWorkflow( "source", "artifacts", "scratch_dir", "source/manifest", osutils=self.osutils, experimental_flags=[], ) self.assertEqual(len(workflow.actions), 3) self.assertIsInstance(workflow.actions[0], CopySourceAction) self.assertIsInstance(workflow.actions[1], NodejsNpmInstallAction) self.assertIsInstance(workflow.actions[2], EsbuildBundleAction) self.osutils.file_exists.assert_has_calls( [call("source/package-lock.json"), call("source/npm-shrinkwrap.json")] ) def test_sets_up_esbuild_search_path_from_npm_bin(self): self.popen.out = b"project/" workflow = NodejsNpmEsbuildWorkflow( "source", "artifacts", "scratch_dir", "source/manifest", osutils=self.osutils, experimental_flags=[], ) self.osutils.popen.assert_called_with(["npm", "root"], stdout="PIPE", stderr="PIPE", cwd="scratch_dir") esbuild = workflow.actions[2]._subprocess_esbuild self.assertIsInstance(esbuild, SubprocessEsbuild) self.assertEqual(esbuild.executable_search_paths, [str(Path("project/.bin"))]) def test_sets_up_esbuild_search_path_with_workflow_executable_search_paths_after_npm_bin(self): self.popen.out = b"project" workflow = NodejsNpmEsbuildWorkflow( "source", "artifacts", "scratch_dir", "source/manifest", osutils=self.osutils, executable_search_paths=["other/bin"], experimental_flags=[], ) self.osutils.popen.assert_called_with(["npm", "root"], stdout="PIPE", stderr="PIPE", cwd="scratch_dir") esbuild = workflow.actions[2]._subprocess_esbuild self.assertIsInstance(esbuild, SubprocessEsbuild) self.assertEqual(esbuild.executable_search_paths, [str(Path("project/.bin")), "other/bin"]) def test_workflow_uses_npm_ci_if_lockfile_exists(self): self.osutils.file_exists.side_effect = [True, True] workflow = NodejsNpmEsbuildWorkflow( "source", "artifacts", "scratch_dir", "source/manifest", osutils=self.osutils, experimental_flags=[], options={"use_npm_ci": True}, ) self.assertEqual(len(workflow.actions), 3) self.assertIsInstance(workflow.actions[0], CopySourceAction) self.assertIsInstance(workflow.actions[1], NodejsNpmCIAction) self.assertIsInstance(workflow.actions[2], EsbuildBundleAction) self.osutils.file_exists.assert_has_calls([call("source/package-lock.json")]) def test_workflow_uses_npm_ci_if_shrinkwrap_exists(self): self.osutils.file_exists.side_effect = [True, False, True] workflow = NodejsNpmEsbuildWorkflow( "source", "artifacts", "scratch_dir", "source/manifest", osutils=self.osutils, experimental_flags=[], options={"use_npm_ci": True}, ) self.assertEqual(len(workflow.actions), 3) self.assertIsInstance(workflow.actions[0], CopySourceAction) self.assertIsInstance(workflow.actions[1], NodejsNpmCIAction) self.assertIsInstance(workflow.actions[2], EsbuildBundleAction) self.osutils.file_exists.assert_has_calls( [call("source/package-lock.json"), call("source/npm-shrinkwrap.json")] ) def test_workflow_doesnt_use_npm_ci_no_options_config(self): self.osutils.file_exists.side_effect = [True, False, True] workflow = NodejsNpmEsbuildWorkflow( "source", "artifacts", "scratch_dir", "source/manifest", osutils=self.osutils, experimental_flags=[], ) self.assertEqual(len(workflow.actions), 3) self.assertIsInstance(workflow.actions[0], CopySourceAction) self.assertIsInstance(workflow.actions[1], NodejsNpmInstallAction) self.assertIsInstance(workflow.actions[2], EsbuildBundleAction) self.osutils.file_exists.assert_has_calls( [call("source/package-lock.json"), call("source/npm-shrinkwrap.json")] ) def test_must_validate_architecture(self): self.osutils.is_windows.side_effect = [False, False] self.osutils.popen.side_effect = [self.popen, self.popen] workflow = NodejsNpmEsbuildWorkflow( "source", "artifacts", "scratch", "source/manifest", options={"artifact_executable_name": "foo"}, osutils=self.osutils, experimental_flags=[], ) workflow_with_arm = NodejsNpmEsbuildWorkflow( "source", "artifacts", "scratch", "source/manifest", architecture=ARM64, osutils=self.osutils, experimental_flags=[], ) self.assertEqual(workflow.architecture, "x86_64") self.assertEqual(workflow_with_arm.architecture, "arm64") def test_workflow_sets_up_esbuild_actions_with_download_dependencies_without_dependencies_dir(self): self.osutils.file_exists.return_value = True workflow = NodejsNpmEsbuildWorkflow( "source", "artifacts", "scratch_dir", "source/manifest", osutils=self.osutils, experimental_flags=[], ) self.assertEqual(len(workflow.actions), 3) self.assertIsInstance(workflow.actions[0], CopySourceAction) self.assertIsInstance(workflow.actions[1], NodejsNpmInstallAction) self.assertIsInstance(workflow.actions[2], EsbuildBundleAction) def test_workflow_sets_up_esbuild_actions_without_download_dependencies_with_dependencies_dir_combine_deps(self): self.osutils.file_exists.return_value = True workflow = NodejsNpmEsbuildWorkflow( "source", "artifacts", "scratch_dir", "source/manifest", dependencies_dir="dep", download_dependencies=False, combine_dependencies=True, osutils=self.osutils, experimental_flags=[], ) self.assertEqual(len(workflow.actions), 3) self.assertIsInstance(workflow.actions[0], CopySourceAction) self.assertIsInstance(workflow.actions[1], LinkSourceAction) self.assertIsInstance(workflow.actions[2], EsbuildBundleAction) def test_workflow_sets_up_esbuild_actions_without_download_dependencies_with_dependencies_dir_no_combine_deps(self): self.osutils.file_exists.return_value = True workflow = NodejsNpmEsbuildWorkflow( "source", "artifacts", "scratch_dir", "source/manifest", dependencies_dir="dep", download_dependencies=False, combine_dependencies=False, osutils=self.osutils, experimental_flags=[], ) self.assertEqual(len(workflow.actions), 3) self.assertIsInstance(workflow.actions[0], CopySourceAction) self.assertIsInstance(workflow.actions[1], LinkSourceAction) self.assertIsInstance(workflow.actions[2], EsbuildBundleAction) def test_workflow_sets_up_esbuild_actions_with_download_dependencies_and_dependencies_dir(self): self.osutils.file_exists.return_value = True workflow = NodejsNpmEsbuildWorkflow( "source", "artifacts", "scratch_dir", "source/manifest", dependencies_dir="dep", download_dependencies=True, osutils=self.osutils, experimental_flags=[], ) self.assertEqual(len(workflow.actions), 5) self.assertIsInstance(workflow.actions[0], CopySourceAction) self.assertIsInstance(workflow.actions[1], NodejsNpmInstallAction) self.assertIsInstance(workflow.actions[2], EsbuildBundleAction) self.assertIsInstance(workflow.actions[3], CleanUpAction) self.assertIsInstance(workflow.actions[4], MoveDependenciesAction) def test_workflow_sets_up_esbuild_actions_with_download_dependencies_and_dependencies_dir_no_combine_deps(self): workflow = NodejsNpmEsbuildWorkflow( "source", "artifacts", "scratch_dir", "source/manifest", dependencies_dir="dep", download_dependencies=True, combine_dependencies=False, osutils=self.osutils, experimental_flags=[], ) self.assertEqual(len(workflow.actions), 5) self.assertIsInstance(workflow.actions[0], CopySourceAction) self.assertIsInstance(workflow.actions[1], NodejsNpmInstallAction) self.assertIsInstance(workflow.actions[2], EsbuildBundleAction) self.assertIsInstance(workflow.actions[3], CleanUpAction) self.assertIsInstance(workflow.actions[4], MoveDependenciesAction) @patch("aws_lambda_builders.workflows.nodejs_npm_esbuild.workflow.NodejsNpmWorkflow") def test_workflow_uses_production_npm_version(self, get_workflow_mock): workflow = NodejsNpmEsbuildWorkflow( "source", "artifacts", "scratch_dir", "source/manifest", dependencies_dir=None, download_dependencies=True, combine_dependencies=False, osutils=self.osutils, experimental_flags=[], ) self.assertEqual(len(workflow.actions), 3) self.assertIsInstance(workflow.actions[0], CopySourceAction) self.assertIsInstance(workflow.actions[2], EsbuildBundleAction) get_workflow_mock.get_install_action.assert_called_with( source_dir="source", install_dir="scratch_dir", subprocess_npm=ANY, osutils=ANY, build_options=None, install_links=False, ) @patch("aws_lambda_builders.workflows.nodejs_npm_esbuild.workflow.NodejsNpmEsbuildWorkflow._get_esbuild_subprocess") @patch("aws_lambda_builders.workflows.nodejs_npm_esbuild.workflow.SubprocessNpm") @patch("aws_lambda_builders.workflows.nodejs_npm_esbuild.workflow.OSUtils") def test_manifest_not_found(self, osutils_mock, subprocess_npm_mock, get_esbuild_subprocess_mock): osutils_mock.file_exists.return_value = False workflow = NodejsNpmEsbuildWorkflow( "source", "artifacts", "scratch_dir", "source/manifest", osutils=osutils_mock, ) self.assertEqual(len(workflow.actions), 1) self.assertIsInstance(workflow.actions[0], EsbuildBundleAction) def test_no_download_dependencies_and_no_dependencies_dir_fails(self): with self.assertRaises(EsbuildExecutionError): NodejsNpmEsbuildWorkflow( "source", "artifacts", "scratch_dir", "source/manifest", osutils=self.osutils, download_dependencies=False, ) def test_build_in_source(self): source_dir = "source" workflow = NodejsNpmEsbuildWorkflow( source_dir=source_dir, artifacts_dir="artifacts", scratch_dir="scratch_dir", manifest_path="source/manifest", osutils=self.osutils, build_in_source=True, ) self.assertEqual(len(workflow.actions), 2) self.assertIsInstance(workflow.actions[0], NodejsNpmInstallAction) self.assertEqual(workflow.actions[0].install_dir, source_dir) self.assertIsInstance(workflow.actions[1], EsbuildBundleAction) self.assertEqual(workflow.actions[1]._working_directory, source_dir) def test_workflow_sets_up_npm_actions_with_download_dependencies_without_dependencies_dir_external_manifest(self): self.osutils.dirname.return_value = "not_source" workflow = NodejsNpmEsbuildWorkflow( source_dir="source", artifacts_dir="artifacts", scratch_dir="scratch_dir", manifest_path="not_source/manifest", osutils=self.osutils, ) self.assertEqual(len(workflow.actions), 4) self.assertIsInstance(workflow.actions[0], CopySourceAction) self.assertIsInstance(workflow.actions[1], CopySourceAction) self.assertEquals(workflow.actions[1].source_dir, "not_source") self.assertEquals(workflow.actions[1].dest_dir, "scratch_dir") self.assertIsInstance(workflow.actions[2], NodejsNpmInstallAction) self.assertEquals(workflow.actions[2].install_dir, "scratch_dir") self.assertIsInstance(workflow.actions[3], EsbuildBundleAction) def test_workflow_sets_up_npm_actions_with_download_dependencies_without_dependencies_dir_external_manifest_and_build_in_source( self, ): self.osutils.dirname.return_value = "not_source" workflow = NodejsNpmEsbuildWorkflow( source_dir="source", artifacts_dir="artifacts", scratch_dir="scratch_dir", manifest_path="not_source/manifest", osutils=self.osutils, build_in_source=True, ) self.assertEqual(len(workflow.actions), 3) self.assertIsInstance(workflow.actions[0], NodejsNpmInstallAction) self.assertEquals(workflow.actions[0].install_dir, "not_source") self.assertIsInstance(workflow.actions[1], LinkSinglePathAction) self.assertEquals(workflow.actions[1]._source, os.path.join("not_source", "node_modules")) self.assertEquals(workflow.actions[1]._dest, os.path.join("source", "node_modules")) self.assertIsInstance(workflow.actions[2], EsbuildBundleAction)