From 478271355ae31faff6eea367fcd3bff3dc662c11 Mon Sep 17 00:00:00 2001 From: Reed Hamilton Date: Fri, 20 Feb 2026 11:02:01 -0800 Subject: [PATCH 1/6] test: add basic integration test --- .../integration/buildcmd/build_integ_base.py | 4 ++ tests/integration/buildcmd/test_build_cmd.py | 46 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/tests/integration/buildcmd/build_integ_base.py b/tests/integration/buildcmd/build_integ_base.py index 8c4dfa2a06..85fc05576d 100644 --- a/tests/integration/buildcmd/build_integ_base.py +++ b/tests/integration/buildcmd/build_integ_base.py @@ -113,6 +113,7 @@ def get_command_list( config_file=None, save_params=False, project_root_dir=None, + use_buildkit=False, ): command_list = [self.cmd, "build"] @@ -139,6 +140,9 @@ def get_command_list( if debug: command_list += ["--debug"] + if use_buildkit: + command_list += ["--use-buildkit"] + if cached: command_list += ["--cached"] diff --git a/tests/integration/buildcmd/test_build_cmd.py b/tests/integration/buildcmd/test_build_cmd.py index 66b6d1ae94..8ae37242e4 100644 --- a/tests/integration/buildcmd/test_build_cmd.py +++ b/tests/integration/buildcmd/test_build_cmd.py @@ -14,6 +14,8 @@ from samcli.lib.utils import osutils from samcli.local.docker.utils import get_validated_container_client +from samcli.local.docker.image_build_client import CLIBuildClient +from samcli.local.docker.container_client_factory import ContainerClientFactory from samcli.yamlhelper import yaml_parse from tests.testing_utils import ( IS_WINDOWS, @@ -1879,6 +1881,50 @@ def _verify_build_succeeds(self, build_dir): self.assertIn("BuildImageFunction", build_dir_files) +@skipIf(SKIP_DOCKER_TESTS, SKIP_DOCKER_MESSAGE) +class TestBuildImageWithBuildkit(BuildIntegBase): + """Test building image functions with buildkit""" + + template = "template_image.yaml" + function_logical_id = "ImageFunction" + + def test_build_image_function_with_buildkit(self): + client = ContainerClientFactory.create_client() + is_available, error_msg = CLIBuildClient.is_available(client.get_runtime_type()) + + if not is_available: + self.skipTest(f"Buildkit not available: {error_msg}") + + tag = uuid4().hex + overrides = { + "Runtime": "3.12", + "Handler": "main.handler", + "DockerFile": "Dockerfile", + "Tag": tag, + } + cmdlist = self.get_command_list(parameter_overrides=overrides, use_buildkit=True) + + command_result = run_command(cmdlist, cwd=self.working_dir) + self.assertEqual(command_result.process.returncode, 0) + + # Verify image was built + self._verify_image_build_artifact( + self.built_template, + self.function_logical_id, + "ImageUri", + f"{self.function_logical_id.lower()}:{tag}", + ) + + # Verify image works + expected = {"pi": "3.14"} + self._verify_invoke_built_function( + self.built_template, + self.function_logical_id, + self._make_parameter_override_arg(overrides), + expected, + ) + + @parameterized_class( ("template", "stack_paths", "layer_full_path", "function_full_paths", "invoke_error_message"), [ From 090c79294172d1e83ec20ed958e8937fa2a9be79 Mon Sep 17 00:00:00 2001 From: Reed Hamilton Date: Mon, 23 Feb 2026 17:16:44 -0800 Subject: [PATCH 2/6] test: more comprehensive integ tests --- samcli/local/docker/image_build_client.py | 4 +- tests/integration/buildcmd/test_build_cmd.py | 90 +++++++++++++++++--- 2 files changed, 80 insertions(+), 14 deletions(-) diff --git a/samcli/local/docker/image_build_client.py b/samcli/local/docker/image_build_client.py index 71508e2ee1..7d3aa2f3c5 100644 --- a/samcli/local/docker/image_build_client.py +++ b/samcli/local/docker/image_build_client.py @@ -203,8 +203,8 @@ def build_image( if process.returncode != 0: raise docker.errors.BuildError(f"Build failed with exit code {process.returncode}", build_log) - for log in build_log: - yield log + # Return a generator that yields the logs + return (log for log in build_log) @staticmethod def is_available(engine_type: str) -> Tuple[bool, Optional[str]]: diff --git a/tests/integration/buildcmd/test_build_cmd.py b/tests/integration/buildcmd/test_build_cmd.py index 8ae37242e4..4a80c1ce3b 100644 --- a/tests/integration/buildcmd/test_build_cmd.py +++ b/tests/integration/buildcmd/test_build_cmd.py @@ -48,9 +48,25 @@ @skipIf(SKIP_DOCKER_TESTS, SKIP_DOCKER_MESSAGE) +@parameterized_class( + ("use_buildkit",), + [ + (False,), + (True,), + ], +) +@pytest.mark.filterwarnings("ignore::ResourceWarning") class TestBuildingImageTypeLambdaDockerFileFailuresContainer(BuildIntegBase): template = "template_image.yaml" + def setUp(self): + super().setUp() + if self.use_buildkit: + client = ContainerClientFactory.create_client() + is_available, error_msg = CLIBuildClient.is_available(client.get_runtime_type()) + if not is_available: + self.skipTest(f"Buildkit not available: {error_msg}") + def test_with_invalid_dockerfile_location(self): overrides = { "Runtime": "3.10", @@ -58,7 +74,7 @@ def test_with_invalid_dockerfile_location(self): "DockerFile": "ThisDockerfileDoesNotExist", "Tag": uuid4().hex, } - cmdlist = self.get_command_list(parameter_overrides=overrides) + cmdlist = self.get_command_list(parameter_overrides=overrides, use_buildkit=self.use_buildkit) command_result = run_command(cmdlist, cwd=self.working_dir) # confirm build failed @@ -79,7 +95,7 @@ def test_with_invalid_dockerfile_definition(self): "DockerFile": "InvalidDockerfile", "Tag": uuid4().hex, } - cmdlist = self.get_command_list(parameter_overrides=overrides) + cmdlist = self.get_command_list(parameter_overrides=overrides, use_buildkit=self.use_buildkit) command_result = run_command(cmdlist, cwd=self.working_dir) # confirm build failed @@ -88,21 +104,37 @@ def test_with_invalid_dockerfile_definition(self): @skipIf(SKIP_DOCKER_TESTS, SKIP_DOCKER_MESSAGE) +@parameterized_class( + ("use_buildkit",), + [ + (False,), + (True,), + ], +) +@pytest.mark.filterwarnings("ignore::ResourceWarning") class TestLoadingImagesFromArchiveContainer(BuildIntegBase): template = "template_loadable_image.yaml" FUNCTION_LOGICAL_ID = "ImageFunction" + def setUp(self): + super().setUp() + if self.use_buildkit: + client = ContainerClientFactory.create_client() + is_available, error_msg = CLIBuildClient.is_available(client.get_runtime_type()) + if not is_available: + self.skipTest(f"Buildkit not available: {error_msg}") + def test_load_not_an_archive_passthrough(self): overrides = {"ImageUri": "./load_image_archive/this_file_does_not_exist.tar.gz"} - cmdlist = self.get_command_list(parameter_overrides=overrides) + cmdlist = self.get_command_list(parameter_overrides=overrides, use_buildkit=self.use_buildkit) command_result = run_command(cmdlist, cwd=self.working_dir) self.assertEqual(command_result.process.returncode, 0) def test_bad_image_archive_fails(self): overrides = {"ImageUri": "./load_image_archive/error.tar.gz"} - cmdlist = self.get_command_list(parameter_overrides=overrides) + cmdlist = self.get_command_list(parameter_overrides=overrides, use_buildkit=self.use_buildkit) command_result = run_command(cmdlist, cwd=self.working_dir) self.assertEqual(command_result.process.returncode, 1) @@ -110,7 +142,7 @@ def test_bad_image_archive_fails(self): def test_load_success(self): overrides = {"ImageUri": "./load_image_archive/archive.tar.gz"} - cmdlist = self.get_command_list(parameter_overrides=overrides) + cmdlist = self.get_command_list(parameter_overrides=overrides, use_buildkit=self.use_buildkit) command_result = run_command(cmdlist, cwd=self.working_dir) self.assertEqual(command_result.process.returncode, 0) @@ -141,17 +173,28 @@ def test_load_success(self): "Skip build tests on windows when running in CI unless overridden", ) @parameterized_class( - ("template", "prop"), + ("template", "prop", "use_buildkit"), [ - ("template_local_prebuilt_image.yaml", "ImageUri"), - ("template_cfn_local_prebuilt_image.yaml", "Code.ImageUri"), + ("template_local_prebuilt_image.yaml", "ImageUri", False), + ("template_cfn_local_prebuilt_image.yaml", "Code.ImageUri", False), + ("template_local_prebuilt_image.yaml", "ImageUri", True), + ("template_cfn_local_prebuilt_image.yaml", "Code.ImageUri", True), ], ) +@pytest.mark.filterwarnings("ignore::ResourceWarning") class TestSkipBuildingFunctionsWithLocalImageUriContainer(BuildIntegBase): EXPECTED_FILES_PROJECT_MANIFEST: Set[str] = set() FUNCTION_LOGICAL_ID_IMAGE = "ImageFunction" + def setUp(self): + super().setUp() + if self.use_buildkit: + client = ContainerClientFactory.create_client() + is_available, error_msg = CLIBuildClient.is_available(client.get_runtime_type()) + if not is_available: + self.skipTest(f"Buildkit not available: {error_msg}") + @parameterized.expand(["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]) def test_with_default_requirements(self, runtime): _tag = uuid4().hex @@ -167,7 +210,7 @@ def test_with_default_requirements(self, runtime): "ImageUri": image_uri, "Handler": "main.handler", } - cmdlist = self.get_command_list(parameter_overrides=overrides) + cmdlist = self.get_command_list(parameter_overrides=overrides, use_buildkit=self.use_buildkit) command_result = run_command(cmdlist, cwd=self.working_dir) self.assertEqual(command_result.process.returncode, 0) @@ -1882,6 +1925,16 @@ def _verify_build_succeeds(self, build_dir): @skipIf(SKIP_DOCKER_TESTS, SKIP_DOCKER_MESSAGE) +@parameterized_class( + ("cached", "parallel", "use_custom_build_dir"), + [ + (False, False, False), # Basic + (True, False, False), # With Caching + (False, True, False), # With parallelism + (False, False, True), # With custom build dir + ], +) +@pytest.mark.filterwarnings("ignore::ResourceWarning") class TestBuildImageWithBuildkit(BuildIntegBase): """Test building image functions with buildkit""" @@ -1895,6 +1948,8 @@ def test_build_image_function_with_buildkit(self): if not is_available: self.skipTest(f"Buildkit not available: {error_msg}") + build_dir = self.custom_build_dir if self.use_custom_build_dir else None + tag = uuid4().hex overrides = { "Runtime": "3.12", @@ -1902,14 +1957,25 @@ def test_build_image_function_with_buildkit(self): "DockerFile": "Dockerfile", "Tag": tag, } - cmdlist = self.get_command_list(parameter_overrides=overrides, use_buildkit=True) + cmdlist = self.get_command_list( + parameter_overrides=overrides, + use_buildkit=True, + cached=self.cached, + parallel=self.parallel, + build_dir=build_dir, + ) command_result = run_command(cmdlist, cwd=self.working_dir) self.assertEqual(command_result.process.returncode, 0) + if self.use_custom_build_dir: + built_template = Path(self.custom_build_dir, "template.yaml") + else: + built_template = self.built_template + # Verify image was built self._verify_image_build_artifact( - self.built_template, + built_template, self.function_logical_id, "ImageUri", f"{self.function_logical_id.lower()}:{tag}", @@ -1918,7 +1984,7 @@ def test_build_image_function_with_buildkit(self): # Verify image works expected = {"pi": "3.14"} self._verify_invoke_built_function( - self.built_template, + built_template, self.function_logical_id, self._make_parameter_override_arg(overrides), expected, From fee82531ebf0cda99ad7540eff843a18ef619574 Mon Sep 17 00:00:00 2001 From: Reed Hamilton Date: Fri, 27 Feb 2026 15:48:08 -0800 Subject: [PATCH 3/6] test: add arm64 test and mark as tier1 --- tests/integration/buildcmd/test_build_cmd.py | 13 ++++++++----- .../testdata/buildcmd/template_image.yaml | 5 +++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/integration/buildcmd/test_build_cmd.py b/tests/integration/buildcmd/test_build_cmd.py index 4a80c1ce3b..07f09f104f 100644 --- a/tests/integration/buildcmd/test_build_cmd.py +++ b/tests/integration/buildcmd/test_build_cmd.py @@ -1926,15 +1926,17 @@ def _verify_build_succeeds(self, build_dir): @skipIf(SKIP_DOCKER_TESTS, SKIP_DOCKER_MESSAGE) @parameterized_class( - ("cached", "parallel", "use_custom_build_dir"), + ("cached", "parallel", "use_custom_build_dir", "architecture"), [ - (False, False, False), # Basic - (True, False, False), # With Caching - (False, True, False), # With parallelism - (False, False, True), # With custom build dir + (False, False, False, "x86_64"), # Basic + (True, False, False, "x86_64"), # With Caching + (False, True, False, "x86_64"), # With parallelism + (False, False, True, "x86_64"), # With custom build dir + (False, False, False, "arm64"), # ARM64 ], ) @pytest.mark.filterwarnings("ignore::ResourceWarning") +@pytest.mark.tier1 class TestBuildImageWithBuildkit(BuildIntegBase): """Test building image functions with buildkit""" @@ -1956,6 +1958,7 @@ def test_build_image_function_with_buildkit(self): "Handler": "main.handler", "DockerFile": "Dockerfile", "Tag": tag, + "Architectures": self.architecture, } cmdlist = self.get_command_list( parameter_overrides=overrides, diff --git a/tests/integration/testdata/buildcmd/template_image.yaml b/tests/integration/testdata/buildcmd/template_image.yaml index 135d9aad67..b512923689 100644 --- a/tests/integration/testdata/buildcmd/template_image.yaml +++ b/tests/integration/testdata/buildcmd/template_image.yaml @@ -10,6 +10,9 @@ Parameters: Type: String Tag: Type: String + Architectures: + Type: String + Default: x86_64 Resources: @@ -21,6 +24,8 @@ Resources: Command: - !Ref Handler Timeout: 600 + Architectures: + - !Ref Architectures Metadata: DockerTag: !Ref Tag DockerContext: ./PythonImage From 2449403db2f5fe226f5928e7cc49d722f36ff12c Mon Sep 17 00:00:00 2001 From: Reed Hamilton Date: Mon, 2 Mar 2026 09:48:00 -0800 Subject: [PATCH 4/6] fix: update dockerfile error messages --- samcli/local/docker/container_client.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/samcli/local/docker/container_client.py b/samcli/local/docker/container_client.py index 6b127802d7..a314ebb943 100644 --- a/samcli/local/docker/container_client.py +++ b/samcli/local/docker/container_client.py @@ -382,7 +382,7 @@ def is_dockerfile_error(self, error: Union[Exception, str]) -> bool: Check if error is a dockerfile-related error for Docker. Docker-specific error patterns for dockerfile-related issues typically - contain "Cannot locate specified Dockerfile" in the error message. + contain "Cannot locate specified Dockerfile" or "failed to read dockerfile" in the error message. Args: error: Exception or error message to check @@ -390,14 +390,15 @@ def is_dockerfile_error(self, error: Union[Exception, str]) -> bool: Returns: bool: True if the error indicates a dockerfile-related issue """ + patterns = ["Cannot locate specified Dockerfile", "failed to read dockerfile"] if isinstance(error, docker.errors.APIError): if not error.is_server_error: return False if not hasattr(error, "explanation") or error.explanation is None: return False - return "Cannot locate specified Dockerfile" in str(error.explanation) + return any(pattern in str(error.explanation) for pattern in patterns) elif isinstance(error, str): - return "Cannot locate specified Dockerfile" in error + return any(pattern in error for pattern in patterns) return False def list_containers_by_image(self, image_name: str, all_containers: bool = True) -> List[Any]: From 07a28252a4b07b55d525167a6df5003e85a16d5e Mon Sep 17 00:00:00 2001 From: Reed Hamilton Date: Mon, 2 Mar 2026 13:07:00 -0800 Subject: [PATCH 5/6] test: remove arm64 test --- tests/integration/buildcmd/test_build_cmd.py | 12 +++++------- .../testdata/buildcmd/template_image.yaml | 5 ----- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/tests/integration/buildcmd/test_build_cmd.py b/tests/integration/buildcmd/test_build_cmd.py index 07f09f104f..28d4d74111 100644 --- a/tests/integration/buildcmd/test_build_cmd.py +++ b/tests/integration/buildcmd/test_build_cmd.py @@ -1926,13 +1926,12 @@ def _verify_build_succeeds(self, build_dir): @skipIf(SKIP_DOCKER_TESTS, SKIP_DOCKER_MESSAGE) @parameterized_class( - ("cached", "parallel", "use_custom_build_dir", "architecture"), + ("cached", "parallel", "use_custom_build_dir"), [ - (False, False, False, "x86_64"), # Basic - (True, False, False, "x86_64"), # With Caching - (False, True, False, "x86_64"), # With parallelism - (False, False, True, "x86_64"), # With custom build dir - (False, False, False, "arm64"), # ARM64 + (False, False, False), # Basic + (True, False, False), # With Caching + (False, True, False), # With parallelism + (False, False, True), # With custom build dir ], ) @pytest.mark.filterwarnings("ignore::ResourceWarning") @@ -1958,7 +1957,6 @@ def test_build_image_function_with_buildkit(self): "Handler": "main.handler", "DockerFile": "Dockerfile", "Tag": tag, - "Architectures": self.architecture, } cmdlist = self.get_command_list( parameter_overrides=overrides, diff --git a/tests/integration/testdata/buildcmd/template_image.yaml b/tests/integration/testdata/buildcmd/template_image.yaml index b512923689..135d9aad67 100644 --- a/tests/integration/testdata/buildcmd/template_image.yaml +++ b/tests/integration/testdata/buildcmd/template_image.yaml @@ -10,9 +10,6 @@ Parameters: Type: String Tag: Type: String - Architectures: - Type: String - Default: x86_64 Resources: @@ -24,8 +21,6 @@ Resources: Command: - !Ref Handler Timeout: 600 - Architectures: - - !Ref Architectures Metadata: DockerTag: !Ref Tag DockerContext: ./PythonImage From bacfe0cae673039e89e37b2f1e8719e118318b21 Mon Sep 17 00:00:00 2001 From: Reed Hamilton Date: Tue, 3 Mar 2026 16:33:51 -0800 Subject: [PATCH 6/6] fix: allow finch without sudo --- tests/setup_finch.sh | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/tests/setup_finch.sh b/tests/setup_finch.sh index 5129284d3e..2a76c82217 100755 --- a/tests/setup_finch.sh +++ b/tests/setup_finch.sh @@ -15,10 +15,10 @@ sudo apt-get autoremove -y || true echo "=== Installing Finch ===" for i in {1..3}; do - if curl -fsSL https://artifact.runfinch.com/deb/GPG_KEY.pub | sudo gpg --dearmor -o /usr/share/keyrings/runfinch-finch-archive-keyring.gpg; then - break - fi - sleep 10 + if curl -fsSL https://artifact.runfinch.com/deb/GPG_KEY.pub | sudo gpg --dearmor -o /usr/share/keyrings/runfinch-finch-archive-keyring.gpg; then + break + fi + sleep 10 done echo 'deb [signed-by=/usr/share/keyrings/runfinch-finch-archive-keyring.gpg arch=amd64] https://artifact.runfinch.com/deb noble main' | sudo tee /etc/apt/sources.list.d/runfinch-finch.list @@ -31,12 +31,16 @@ sudo systemctl enable --now finch-buildkit sleep 3 sudo chmod 666 /var/run/finch.sock +echo "=== Configuring finch for non-root access ===" +sudo chmod +s /usr/libexec/finch/nerdctl +sudo chmod +s /usr/bin/finch + echo "=== Waiting for Finch to be ready ===" for i in {1..12}; do - if sudo finch info >/dev/null 2>&1; then - break - fi - sleep 5 + if sudo finch info >/dev/null 2>&1; then + break + fi + sleep 5 done echo "=== Configuring buildkit sockets ===" @@ -52,4 +56,5 @@ sudo finch run --privileged --rm tonistiigi/binfmt:master --install all echo "=== Finch setup complete ===" sudo finch info -sudo finch version +# Run finch without sudo here to confirm that it's not required +finch version