diff --git a/aws_lambda_builders/workflows/nodejs_npm/actions.py b/aws_lambda_builders/workflows/nodejs_npm/actions.py index 2b7dc656..f6d76e0f 100644 --- a/aws_lambda_builders/workflows/nodejs_npm/actions.py +++ b/aws_lambda_builders/workflows/nodejs_npm/actions.py @@ -3,6 +3,7 @@ """ import logging +import os from typing import Optional from aws_lambda_builders.actions import ActionFailedError, BaseAction, Purpose @@ -321,3 +322,32 @@ def execute(self): except OSError as ex: raise ActionFailedError(str(ex)) + + +class NodejsNpmTestAction(NodejsNpmInstallOrUpdateBaseAction): + """ + A Lambda Builder Action that runs tests in NPM project + """ + + NAME = "NpmTest" + DESCRIPTION = "Running tests from NPM" + + def execute(self): + """ + Runs the action if environment variable `SAM_NPM_RUN_TEST_WITH_BUILD` is `true`. + + :raises lambda_builders.actions.ActionFailedError: when NPM execution fails + """ + try: + is_run_test_with_build = os.getenv("SAM_NPM_RUN_TEST_WITH_BUILD", "False") + if is_run_test_with_build == "true": + LOG.debug("NODEJS running tests in: %s", self.install_dir) + + command = ["test", "--if-present"] + self.subprocess_npm.run(command, cwd=self.install_dir) + else: + LOG.debug("NODEJS skipping tests") + LOG.debug("Add env variable 'SAM_NPM_RUN_TEST_WITH_BUILD=true' to run tests with build") + + except NpmExecutionError as ex: + raise ActionFailedError(str(ex)) diff --git a/aws_lambda_builders/workflows/nodejs_npm/workflow.py b/aws_lambda_builders/workflows/nodejs_npm/workflow.py index 894045ce..83aab816 100644 --- a/aws_lambda_builders/workflows/nodejs_npm/workflow.py +++ b/aws_lambda_builders/workflows/nodejs_npm/workflow.py @@ -22,6 +22,7 @@ NodejsNpmPackAction, NodejsNpmrcAndLockfileCopyAction, NodejsNpmrcCleanUpAction, + NodejsNpmTestAction, NodejsNpmUpdateAction, ) from aws_lambda_builders.workflows.nodejs_npm.npm import SubprocessNpm @@ -123,6 +124,13 @@ def __init__(self, source_dir, artifacts_dir, scratch_dir, manifest_path, runtim ) ) + self.actions.append( + NodejsNpmTestAction( + install_dir=self.manifest_dir if is_building_in_source and is_external_manifest else self.build_dir, + subprocess_npm=subprocess_npm, + ) + ) + if is_building_in_source and is_external_manifest: # Since we run `npm install` in the manifest directory, so we need to link the node_modules directory in # the source directory. diff --git a/tests/integration/workflows/nodejs_npm/test_nodejs_npm.py b/tests/integration/workflows/nodejs_npm/test_nodejs_npm.py index bfb7f0ec..00f8a09b 100644 --- a/tests/integration/workflows/nodejs_npm/test_nodejs_npm.py +++ b/tests/integration/workflows/nodejs_npm/test_nodejs_npm.py @@ -779,3 +779,35 @@ def test_builds_project_with_manifest_outside_root_and_local_dependencies_with_d # expected dependencies in source directory source_modules = set(os.listdir(os.path.join(source_dir, "node_modules"))) self.assertTrue(all(expected_module in source_modules for expected_module in expected_modules)) + + @parameterized.expand([("nodejs16.x",), ("nodejs18.x",), ("nodejs20.x",)]) + def test_runs_test_script_if_specified(self, runtime): + source_dir = os.path.join(self.TEST_DATA_FOLDER, "test-script-to-create-file") + + self.builder.build( + source_dir, + self.artifacts_dir, + self.scratch_dir, + os.path.join(source_dir, "package.json"), + runtime=runtime, + ) + + expected_files = {"package.json", "created.js"} + output_files = set(os.listdir(self.artifacts_dir)) + self.assertEqual(expected_files, output_files) + + @parameterized.expand([("nodejs16.x",), ("nodejs18.x",), ("nodejs20.x",)]) + def test_does_not_raise_error_if_empty_test_script(self, runtime): + source_dir = os.path.join(self.TEST_DATA_FOLDER, "empty-test-script") + + self.builder.build( + source_dir, + self.artifacts_dir, + self.scratch_dir, + os.path.join(source_dir, "package.json"), + runtime=runtime, + ) + + expected_files = {"package.json"} + output_files = set(os.listdir(self.artifacts_dir)) + self.assertEqual(expected_files, output_files) diff --git a/tests/integration/workflows/nodejs_npm/testdata/empty-test-script/package.json b/tests/integration/workflows/nodejs_npm/testdata/empty-test-script/package.json new file mode 100644 index 00000000..f71e0863 --- /dev/null +++ b/tests/integration/workflows/nodejs_npm/testdata/empty-test-script/package.json @@ -0,0 +1,11 @@ +{ + "name": "testscript", + "version": "1.0.0", + "description": "", + "scripts": { + "test": "" + }, + "keywords": [], + "author": "", + "license": "APACHE2.0" +} diff --git a/tests/integration/workflows/nodejs_npm/testdata/test-script-to-create-file/package.json b/tests/integration/workflows/nodejs_npm/testdata/test-script-to-create-file/package.json new file mode 100644 index 00000000..64de691d --- /dev/null +++ b/tests/integration/workflows/nodejs_npm/testdata/test-script-to-create-file/package.json @@ -0,0 +1,11 @@ +{ + "name": "testscript", + "version": "1.0.0", + "description": "", + "scripts": { + "test": "touch created.js" + }, + "keywords": [], + "author": "", + "license": "APACHE2.0" +} diff --git a/tests/unit/workflows/nodejs_npm/test_actions.py b/tests/unit/workflows/nodejs_npm/test_actions.py index d7b77992..ca33237f 100644 --- a/tests/unit/workflows/nodejs_npm/test_actions.py +++ b/tests/unit/workflows/nodejs_npm/test_actions.py @@ -11,6 +11,7 @@ NodejsNpmrcCleanUpAction, NodejsNpmLockFileCleanUpAction, NodejsNpmCIAction, + NodejsNpmTestAction, ) from aws_lambda_builders.workflows.nodejs_npm.npm import NpmExecutionError @@ -219,3 +220,43 @@ def test_raises_action_failed_when_removing_fails(self, OSUtilMock): with self.assertRaises(ActionFailedError): action.execute() + + +class TestNodejsNpmTestAction(TestCase): + @patch("aws_lambda_builders.workflows.nodejs_npm.npm.SubprocessNpm") + @patch.dict("os.environ", {"SAM_NPM_RUN_TEST_WITH_BUILD": "true"}, clear=True) + def test_runs_npm_test_for_npm_project_if_env_var_true(self, SubprocessNpmMock): + subprocess_npm = SubprocessNpmMock.return_value + + action = NodejsNpmTestAction(install_dir="tests", subprocess_npm=subprocess_npm) + + action.execute() + + expected_args = ["test", "--if-present"] + + subprocess_npm.run.assert_called_with(expected_args, cwd="tests") + + @patch("aws_lambda_builders.workflows.nodejs_npm.npm.SubprocessNpm") + def test_does_not_run_npm_test_for_npm_project_if_no_env_var(self, SubprocessNpmMock): + subprocess_npm = SubprocessNpmMock.return_value + + action = NodejsNpmTestAction(install_dir="tests", subprocess_npm=subprocess_npm) + + action.execute() + + assert not subprocess_npm.run.called + + @patch("aws_lambda_builders.workflows.nodejs_npm.npm.SubprocessNpm") + @patch.dict("os.environ", {"SAM_NPM_RUN_TEST_WITH_BUILD": "true"}, clear=True) + def test_raises_action_failed_when_npm_test_fails(self, SubprocessNpmMock): + subprocess_npm = SubprocessNpmMock.return_value + + builder_instance = SubprocessNpmMock.return_value + builder_instance.run.side_effect = NpmExecutionError(message="boom!") + + action = NodejsNpmTestAction("artifacts", subprocess_npm=subprocess_npm) + + with self.assertRaises(ActionFailedError) as raised: + action.execute() + + self.assertEqual(raised.exception.args[0], "NPM Failed: boom!") diff --git a/tests/unit/workflows/nodejs_npm/test_workflow.py b/tests/unit/workflows/nodejs_npm/test_workflow.py index 302c75b6..85133dc7 100644 --- a/tests/unit/workflows/nodejs_npm/test_workflow.py +++ b/tests/unit/workflows/nodejs_npm/test_workflow.py @@ -21,6 +21,7 @@ NodejsNpmLockFileCleanUpAction, NodejsNpmCIAction, NodejsNpmUpdateAction, + NodejsNpmTestAction, ) @@ -57,13 +58,14 @@ def test_workflow_sets_up_npm_actions_with_download_dependencies_without_depende workflow = NodejsNpmWorkflow("source", "artifacts", "scratch_dir", "source/manifest", osutils=self.osutils) - self.assertEqual(len(workflow.actions), 6) + self.assertEqual(len(workflow.actions), 7) self.assertIsInstance(workflow.actions[0], NodejsNpmPackAction) self.assertIsInstance(workflow.actions[1], NodejsNpmrcAndLockfileCopyAction) self.assertIsInstance(workflow.actions[2], CopySourceAction) self.assertIsInstance(workflow.actions[3], NodejsNpmInstallAction) - self.assertIsInstance(workflow.actions[4], NodejsNpmrcCleanUpAction) - self.assertIsInstance(workflow.actions[5], NodejsNpmLockFileCleanUpAction) + self.assertIsInstance(workflow.actions[4], NodejsNpmTestAction) + self.assertIsInstance(workflow.actions[5], NodejsNpmrcCleanUpAction) + self.assertIsInstance(workflow.actions[6], NodejsNpmLockFileCleanUpAction) def test_workflow_sets_up_npm_actions_with_download_dependencies_without_dependencies_dir_external_manifest(self): self.osutils.dirname.return_value = "not_source" @@ -73,7 +75,7 @@ def test_workflow_sets_up_npm_actions_with_download_dependencies_without_depende workflow = NodejsNpmWorkflow("source", "artifacts", "scratch_dir", "not_source/manifest", osutils=self.osutils) - self.assertEqual(len(workflow.actions), 7) + self.assertEqual(len(workflow.actions), 8) self.assertIsInstance(workflow.actions[0], NodejsNpmPackAction) self.assertIsInstance(workflow.actions[1], NodejsNpmrcAndLockfileCopyAction) self.assertIsInstance(workflow.actions[2], CopySourceAction) @@ -82,8 +84,10 @@ def test_workflow_sets_up_npm_actions_with_download_dependencies_without_depende self.assertEqual(workflow.actions[3].dest_dir, "artifacts") self.assertIsInstance(workflow.actions[4], NodejsNpmInstallAction) self.assertEqual(workflow.actions[4].install_dir, "artifacts") - self.assertIsInstance(workflow.actions[5], NodejsNpmrcCleanUpAction) - self.assertIsInstance(workflow.actions[6], NodejsNpmLockFileCleanUpAction) + self.assertIsInstance(workflow.actions[5], NodejsNpmTestAction) + self.assertEqual(workflow.actions[5].install_dir, "artifacts") + self.assertIsInstance(workflow.actions[6], NodejsNpmrcCleanUpAction) + self.assertIsInstance(workflow.actions[7], NodejsNpmLockFileCleanUpAction) @patch("aws_lambda_builders.workflows.nodejs_npm.workflow.NodejsNpmWorkflow.can_use_install_links") def test_workflow_sets_up_npm_actions_with_download_dependencies_without_dependencies_dir_external_manifest_and_build_in_source( @@ -100,7 +104,7 @@ def test_workflow_sets_up_npm_actions_with_download_dependencies_without_depende "source", "artifacts", "scratch_dir", "not_source/manifest", osutils=self.osutils, build_in_source=True ) - self.assertEqual(len(workflow.actions), 8) + self.assertEqual(len(workflow.actions), 9) self.assertIsInstance(workflow.actions[0], NodejsNpmPackAction) self.assertIsInstance(workflow.actions[1], NodejsNpmrcAndLockfileCopyAction) self.assertIsInstance(workflow.actions[2], CopySourceAction) @@ -109,13 +113,15 @@ def test_workflow_sets_up_npm_actions_with_download_dependencies_without_depende self.assertEqual(workflow.actions[3].dest_dir, "artifacts") self.assertIsInstance(workflow.actions[4], NodejsNpmUpdateAction) self.assertEqual(workflow.actions[4].install_dir, "not_source") - self.assertIsInstance(workflow.actions[5], LinkSinglePathAction) - self.assertEqual(workflow.actions[5]._source, os.path.join("not_source", "node_modules")) - self.assertEqual(workflow.actions[5]._dest, os.path.join("source", "node_modules")) + self.assertIsInstance(workflow.actions[5], NodejsNpmTestAction) + self.assertEqual(workflow.actions[5].install_dir, "not_source") self.assertIsInstance(workflow.actions[6], LinkSinglePathAction) - self.assertEqual(workflow.actions[6]._source, os.path.join("source", "node_modules")) - self.assertEqual(workflow.actions[6]._dest, os.path.join("artifacts", "node_modules")) - self.assertIsInstance(workflow.actions[7], NodejsNpmrcCleanUpAction) + self.assertEqual(workflow.actions[6]._source, os.path.join("not_source", "node_modules")) + self.assertEqual(workflow.actions[6]._dest, os.path.join("source", "node_modules")) + self.assertIsInstance(workflow.actions[7], LinkSinglePathAction) + self.assertEqual(workflow.actions[7]._source, os.path.join("source", "node_modules")) + self.assertEqual(workflow.actions[7]._dest, os.path.join("artifacts", "node_modules")) + self.assertIsInstance(workflow.actions[8], NodejsNpmrcCleanUpAction) def test_workflow_sets_up_npm_actions_without_download_dependencies_with_dependencies_dir(self): self.osutils.file_exists.return_value = True @@ -145,14 +151,15 @@ def test_workflow_sets_up_npm_actions_without_bundler_if_manifest_doesnt_request workflow = NodejsNpmWorkflow("source", "artifacts", "scratch_dir", "source/manifest", osutils=self.osutils) - self.assertEqual(len(workflow.actions), 6) + self.assertEqual(len(workflow.actions), 7) self.assertIsInstance(workflow.actions[0], NodejsNpmPackAction) self.assertIsInstance(workflow.actions[1], NodejsNpmrcAndLockfileCopyAction) self.assertIsInstance(workflow.actions[2], CopySourceAction) self.assertIsInstance(workflow.actions[3], NodejsNpmInstallAction) - self.assertIsInstance(workflow.actions[4], NodejsNpmrcCleanUpAction) - self.assertIsInstance(workflow.actions[5], NodejsNpmLockFileCleanUpAction) + self.assertIsInstance(workflow.actions[4], NodejsNpmTestAction) + self.assertIsInstance(workflow.actions[5], NodejsNpmrcCleanUpAction) + self.assertIsInstance(workflow.actions[6], NodejsNpmLockFileCleanUpAction) def test_workflow_sets_up_npm_actions_with_download_dependencies_and_dependencies_dir(self): self.osutils.file_exists.side_effect = [True, False, False] @@ -167,17 +174,18 @@ def test_workflow_sets_up_npm_actions_with_download_dependencies_and_dependencie osutils=self.osutils, ) - self.assertEqual(len(workflow.actions), 9) + self.assertEqual(len(workflow.actions), 10) self.assertIsInstance(workflow.actions[0], NodejsNpmPackAction) self.assertIsInstance(workflow.actions[1], NodejsNpmrcAndLockfileCopyAction) self.assertIsInstance(workflow.actions[2], CopySourceAction) self.assertIsInstance(workflow.actions[3], NodejsNpmInstallAction) - self.assertIsInstance(workflow.actions[4], CleanUpAction) - self.assertIsInstance(workflow.actions[5], CopyDependenciesAction) - self.assertIsInstance(workflow.actions[6], NodejsNpmrcCleanUpAction) - self.assertIsInstance(workflow.actions[7], NodejsNpmLockFileCleanUpAction) + self.assertIsInstance(workflow.actions[4], NodejsNpmTestAction) + self.assertIsInstance(workflow.actions[5], CleanUpAction) + self.assertIsInstance(workflow.actions[6], CopyDependenciesAction) + self.assertIsInstance(workflow.actions[7], NodejsNpmrcCleanUpAction) self.assertIsInstance(workflow.actions[8], NodejsNpmLockFileCleanUpAction) + self.assertIsInstance(workflow.actions[9], NodejsNpmLockFileCleanUpAction) def test_workflow_sets_up_npm_actions_without_download_dependencies_and_without_dependencies_dir(self): workflow = NodejsNpmWorkflow( @@ -212,17 +220,18 @@ def test_workflow_sets_up_npm_actions_without_combine_dependencies(self): osutils=self.osutils, ) - self.assertEqual(len(workflow.actions), 9) + self.assertEqual(len(workflow.actions), 10) self.assertIsInstance(workflow.actions[0], NodejsNpmPackAction) self.assertIsInstance(workflow.actions[1], NodejsNpmrcAndLockfileCopyAction) self.assertIsInstance(workflow.actions[2], CopySourceAction) self.assertIsInstance(workflow.actions[3], NodejsNpmInstallAction) - self.assertIsInstance(workflow.actions[4], CleanUpAction) - self.assertIsInstance(workflow.actions[5], MoveDependenciesAction) - self.assertIsInstance(workflow.actions[6], NodejsNpmrcCleanUpAction) - self.assertIsInstance(workflow.actions[7], NodejsNpmLockFileCleanUpAction) + self.assertIsInstance(workflow.actions[4], NodejsNpmTestAction) + self.assertIsInstance(workflow.actions[5], CleanUpAction) + self.assertIsInstance(workflow.actions[6], MoveDependenciesAction) + self.assertIsInstance(workflow.actions[7], NodejsNpmrcCleanUpAction) self.assertIsInstance(workflow.actions[8], NodejsNpmLockFileCleanUpAction) + self.assertIsInstance(workflow.actions[9], NodejsNpmLockFileCleanUpAction) def test_must_validate_architecture(self): self.osutils.is_windows.side_effect = [False, False] @@ -258,13 +267,14 @@ def test_workflow_uses_npm_ci_if_shrinkwrap_exists_and_npm_ci_enabled(self): options={"use_npm_ci": True}, ) - self.assertEqual(len(workflow.actions), 6) + self.assertEqual(len(workflow.actions), 7) self.assertIsInstance(workflow.actions[0], NodejsNpmPackAction) self.assertIsInstance(workflow.actions[1], NodejsNpmrcAndLockfileCopyAction) self.assertIsInstance(workflow.actions[2], CopySourceAction) self.assertIsInstance(workflow.actions[3], NodejsNpmCIAction) - self.assertIsInstance(workflow.actions[4], NodejsNpmrcCleanUpAction) - self.assertIsInstance(workflow.actions[5], NodejsNpmLockFileCleanUpAction) + self.assertIsInstance(workflow.actions[4], NodejsNpmTestAction) + self.assertIsInstance(workflow.actions[5], NodejsNpmrcCleanUpAction) + self.assertIsInstance(workflow.actions[6], NodejsNpmLockFileCleanUpAction) self.osutils.file_exists.assert_has_calls( [call("source/package-lock.json"), call("source/npm-shrinkwrap.json")] ) @@ -281,13 +291,14 @@ def test_workflow_uses_npm_ci_if_lockfile_exists_and_npm_ci_enabled(self): options={"use_npm_ci": True}, ) - self.assertEqual(len(workflow.actions), 6) + self.assertEqual(len(workflow.actions), 7) self.assertIsInstance(workflow.actions[0], NodejsNpmPackAction) self.assertIsInstance(workflow.actions[1], NodejsNpmrcAndLockfileCopyAction) self.assertIsInstance(workflow.actions[2], CopySourceAction) self.assertIsInstance(workflow.actions[3], NodejsNpmCIAction) - self.assertIsInstance(workflow.actions[4], NodejsNpmrcCleanUpAction) - self.assertIsInstance(workflow.actions[5], NodejsNpmLockFileCleanUpAction) + self.assertIsInstance(workflow.actions[4], NodejsNpmTestAction) + self.assertIsInstance(workflow.actions[5], NodejsNpmrcCleanUpAction) + self.assertIsInstance(workflow.actions[6], NodejsNpmLockFileCleanUpAction) self.osutils.file_exists.assert_has_calls([call("source/package-lock.json")]) @patch("aws_lambda_builders.workflows.nodejs_npm.workflow.NodejsNpmWorkflow.can_use_install_links") @@ -327,16 +338,18 @@ def test_build_in_source_with_download_dependencies(self, can_use_links_mock): build_in_source=True, ) - self.assertEqual(len(workflow.actions), 6) + self.assertEqual(len(workflow.actions), 7) self.assertIsInstance(workflow.actions[0], NodejsNpmPackAction) self.assertIsInstance(workflow.actions[1], NodejsNpmrcAndLockfileCopyAction) self.assertIsInstance(workflow.actions[2], CopySourceAction) self.assertIsInstance(workflow.actions[3], NodejsNpmUpdateAction) self.assertEqual(workflow.actions[3].install_dir, source_dir) - self.assertIsInstance(workflow.actions[4], LinkSinglePathAction) - self.assertEqual(workflow.actions[4]._source, os.path.join(source_dir, "node_modules")) - self.assertEqual(workflow.actions[4]._dest, os.path.join(artifacts_dir, "node_modules")) - self.assertIsInstance(workflow.actions[5], NodejsNpmrcCleanUpAction) + self.assertIsInstance(workflow.actions[4], NodejsNpmTestAction) + self.assertEqual(workflow.actions[4].install_dir, source_dir) + self.assertIsInstance(workflow.actions[5], LinkSinglePathAction) + self.assertEqual(workflow.actions[5]._source, os.path.join(source_dir, "node_modules")) + self.assertEqual(workflow.actions[5]._dest, os.path.join(artifacts_dir, "node_modules")) + self.assertIsInstance(workflow.actions[6], NodejsNpmrcCleanUpAction) @patch("aws_lambda_builders.workflows.nodejs_npm.workflow.NodejsNpmWorkflow.can_use_install_links") def test_build_in_source_with_download_dependencies_and_dependencies_dir(self, can_use_links_mock): @@ -354,18 +367,20 @@ def test_build_in_source_with_download_dependencies_and_dependencies_dir(self, c dependencies_dir="dep", ) - self.assertEqual(len(workflow.actions), 8) + self.assertEqual(len(workflow.actions), 9) self.assertIsInstance(workflow.actions[0], NodejsNpmPackAction) self.assertIsInstance(workflow.actions[1], NodejsNpmrcAndLockfileCopyAction) self.assertIsInstance(workflow.actions[2], CopySourceAction) self.assertIsInstance(workflow.actions[3], NodejsNpmUpdateAction) self.assertEqual(workflow.actions[3].install_dir, source_dir) - self.assertIsInstance(workflow.actions[4], LinkSinglePathAction) - self.assertEqual(workflow.actions[4]._source, os.path.join(source_dir, "node_modules")) - self.assertEqual(workflow.actions[4]._dest, os.path.join(artifacts_dir, "node_modules")) - self.assertIsInstance(workflow.actions[5], CleanUpAction) - self.assertIsInstance(workflow.actions[6], CopyDependenciesAction) - self.assertIsInstance(workflow.actions[7], NodejsNpmrcCleanUpAction) + self.assertIsInstance(workflow.actions[4], NodejsNpmTestAction) + self.assertEqual(workflow.actions[4].install_dir, source_dir) + self.assertIsInstance(workflow.actions[5], LinkSinglePathAction) + self.assertEqual(workflow.actions[5]._source, os.path.join(source_dir, "node_modules")) + self.assertEqual(workflow.actions[5]._dest, os.path.join(artifacts_dir, "node_modules")) + self.assertIsInstance(workflow.actions[6], CleanUpAction) + self.assertIsInstance(workflow.actions[7], CopyDependenciesAction) + self.assertIsInstance(workflow.actions[8], NodejsNpmrcCleanUpAction) @patch("aws_lambda_builders.workflows.nodejs_npm.workflow.NodejsNpmWorkflow.can_use_install_links") def test_build_in_source_with_dependencies_dir(self, can_use_links_mock):