Skip to content

Commit b9aee06

Browse files
authored
test: add integration tests for CLI client (#8689)
* test: add basic integration test * test: more comprehensive integ tests * test: add arm64 test and mark as tier1 * fix: update dockerfile error messages * test: remove arm64 test * fix: allow finch without sudo
1 parent b230c66 commit b9aee06

5 files changed

Lines changed: 146 additions & 23 deletions

File tree

samcli/local/docker/container_client.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -382,22 +382,23 @@ def is_dockerfile_error(self, error: Union[Exception, str]) -> bool:
382382
Check if error is a dockerfile-related error for Docker.
383383
384384
Docker-specific error patterns for dockerfile-related issues typically
385-
contain "Cannot locate specified Dockerfile" in the error message.
385+
contain "Cannot locate specified Dockerfile" or "failed to read dockerfile" in the error message.
386386
387387
Args:
388388
error: Exception or error message to check
389389
390390
Returns:
391391
bool: True if the error indicates a dockerfile-related issue
392392
"""
393+
patterns = ["Cannot locate specified Dockerfile", "failed to read dockerfile"]
393394
if isinstance(error, docker.errors.APIError):
394395
if not error.is_server_error:
395396
return False
396397
if not hasattr(error, "explanation") or error.explanation is None:
397398
return False
398-
return "Cannot locate specified Dockerfile" in str(error.explanation)
399+
return any(pattern in str(error.explanation) for pattern in patterns)
399400
elif isinstance(error, str):
400-
return "Cannot locate specified Dockerfile" in error
401+
return any(pattern in error for pattern in patterns)
401402
return False
402403

403404
def list_containers_by_image(self, image_name: str, all_containers: bool = True) -> List[Any]:

samcli/local/docker/image_build_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,8 @@ def build_image(
203203
if process.returncode != 0:
204204
raise docker.errors.BuildError(f"Build failed with exit code {process.returncode}", build_log)
205205

206-
for log in build_log:
207-
yield log
206+
# Return a generator that yields the logs
207+
return (log for log in build_log)
208208

209209
@staticmethod
210210
def is_available(engine_type: str) -> Tuple[bool, Optional[str]]:

tests/integration/buildcmd/build_integ_base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ def get_command_list(
113113
config_file=None,
114114
save_params=False,
115115
project_root_dir=None,
116+
use_buildkit=False,
116117
):
117118
command_list = [self.cmd, "build"]
118119

@@ -139,6 +140,9 @@ def get_command_list(
139140
if debug:
140141
command_list += ["--debug"]
141142

143+
if use_buildkit:
144+
command_list += ["--use-buildkit"]
145+
142146
if cached:
143147
command_list += ["--cached"]
144148

tests/integration/buildcmd/test_build_cmd.py

Lines changed: 122 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
from samcli.lib.utils import osutils
1616
from samcli.local.docker.utils import get_validated_container_client
17+
from samcli.local.docker.image_build_client import CLIBuildClient
18+
from samcli.local.docker.container_client_factory import ContainerClientFactory
1719
from samcli.yamlhelper import yaml_parse
1820
from tests.testing_utils import (
1921
IS_WINDOWS,
@@ -46,17 +48,33 @@
4648

4749

4850
@skipIf(SKIP_DOCKER_TESTS, SKIP_DOCKER_MESSAGE)
51+
@parameterized_class(
52+
("use_buildkit",),
53+
[
54+
(False,),
55+
(True,),
56+
],
57+
)
58+
@pytest.mark.filterwarnings("ignore::ResourceWarning")
4959
class TestBuildingImageTypeLambdaDockerFileFailuresContainer(BuildIntegBase):
5060
template = "template_image.yaml"
5161

62+
def setUp(self):
63+
super().setUp()
64+
if self.use_buildkit:
65+
client = ContainerClientFactory.create_client()
66+
is_available, error_msg = CLIBuildClient.is_available(client.get_runtime_type())
67+
if not is_available:
68+
self.skipTest(f"Buildkit not available: {error_msg}")
69+
5270
def test_with_invalid_dockerfile_location(self):
5371
overrides = {
5472
"Runtime": "3.10",
5573
"Handler": "handler",
5674
"DockerFile": "ThisDockerfileDoesNotExist",
5775
"Tag": uuid4().hex,
5876
}
59-
cmdlist = self.get_command_list(parameter_overrides=overrides)
77+
cmdlist = self.get_command_list(parameter_overrides=overrides, use_buildkit=self.use_buildkit)
6078
command_result = run_command(cmdlist, cwd=self.working_dir)
6179

6280
# confirm build failed
@@ -77,7 +95,7 @@ def test_with_invalid_dockerfile_definition(self):
7795
"DockerFile": "InvalidDockerfile",
7896
"Tag": uuid4().hex,
7997
}
80-
cmdlist = self.get_command_list(parameter_overrides=overrides)
98+
cmdlist = self.get_command_list(parameter_overrides=overrides, use_buildkit=self.use_buildkit)
8199
command_result = run_command(cmdlist, cwd=self.working_dir)
82100

83101
# confirm build failed
@@ -86,29 +104,45 @@ def test_with_invalid_dockerfile_definition(self):
86104

87105

88106
@skipIf(SKIP_DOCKER_TESTS, SKIP_DOCKER_MESSAGE)
107+
@parameterized_class(
108+
("use_buildkit",),
109+
[
110+
(False,),
111+
(True,),
112+
],
113+
)
114+
@pytest.mark.filterwarnings("ignore::ResourceWarning")
89115
class TestLoadingImagesFromArchiveContainer(BuildIntegBase):
90116
template = "template_loadable_image.yaml"
91117

92118
FUNCTION_LOGICAL_ID = "ImageFunction"
93119

120+
def setUp(self):
121+
super().setUp()
122+
if self.use_buildkit:
123+
client = ContainerClientFactory.create_client()
124+
is_available, error_msg = CLIBuildClient.is_available(client.get_runtime_type())
125+
if not is_available:
126+
self.skipTest(f"Buildkit not available: {error_msg}")
127+
94128
def test_load_not_an_archive_passthrough(self):
95129
overrides = {"ImageUri": "./load_image_archive/this_file_does_not_exist.tar.gz"}
96-
cmdlist = self.get_command_list(parameter_overrides=overrides)
130+
cmdlist = self.get_command_list(parameter_overrides=overrides, use_buildkit=self.use_buildkit)
97131
command_result = run_command(cmdlist, cwd=self.working_dir)
98132

99133
self.assertEqual(command_result.process.returncode, 0)
100134

101135
def test_bad_image_archive_fails(self):
102136
overrides = {"ImageUri": "./load_image_archive/error.tar.gz"}
103-
cmdlist = self.get_command_list(parameter_overrides=overrides)
137+
cmdlist = self.get_command_list(parameter_overrides=overrides, use_buildkit=self.use_buildkit)
104138
command_result = run_command(cmdlist, cwd=self.working_dir)
105139

106140
self.assertEqual(command_result.process.returncode, 1)
107141
self.assertIn("unexpected EOF", command_result.stderr.decode())
108142

109143
def test_load_success(self):
110144
overrides = {"ImageUri": "./load_image_archive/archive.tar.gz"}
111-
cmdlist = self.get_command_list(parameter_overrides=overrides)
145+
cmdlist = self.get_command_list(parameter_overrides=overrides, use_buildkit=self.use_buildkit)
112146
command_result = run_command(cmdlist, cwd=self.working_dir)
113147

114148
self.assertEqual(command_result.process.returncode, 0)
@@ -139,17 +173,28 @@ def test_load_success(self):
139173
"Skip build tests on windows when running in CI unless overridden",
140174
)
141175
@parameterized_class(
142-
("template", "prop"),
176+
("template", "prop", "use_buildkit"),
143177
[
144-
("template_local_prebuilt_image.yaml", "ImageUri"),
145-
("template_cfn_local_prebuilt_image.yaml", "Code.ImageUri"),
178+
("template_local_prebuilt_image.yaml", "ImageUri", False),
179+
("template_cfn_local_prebuilt_image.yaml", "Code.ImageUri", False),
180+
("template_local_prebuilt_image.yaml", "ImageUri", True),
181+
("template_cfn_local_prebuilt_image.yaml", "Code.ImageUri", True),
146182
],
147183
)
184+
@pytest.mark.filterwarnings("ignore::ResourceWarning")
148185
class TestSkipBuildingFunctionsWithLocalImageUriContainer(BuildIntegBase):
149186
EXPECTED_FILES_PROJECT_MANIFEST: Set[str] = set()
150187

151188
FUNCTION_LOGICAL_ID_IMAGE = "ImageFunction"
152189

190+
def setUp(self):
191+
super().setUp()
192+
if self.use_buildkit:
193+
client = ContainerClientFactory.create_client()
194+
is_available, error_msg = CLIBuildClient.is_available(client.get_runtime_type())
195+
if not is_available:
196+
self.skipTest(f"Buildkit not available: {error_msg}")
197+
153198
@parameterized.expand(["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"])
154199
def test_with_default_requirements(self, runtime):
155200
_tag = uuid4().hex
@@ -165,7 +210,7 @@ def test_with_default_requirements(self, runtime):
165210
"ImageUri": image_uri,
166211
"Handler": "main.handler",
167212
}
168-
cmdlist = self.get_command_list(parameter_overrides=overrides)
213+
cmdlist = self.get_command_list(parameter_overrides=overrides, use_buildkit=self.use_buildkit)
169214

170215
command_result = run_command(cmdlist, cwd=self.working_dir)
171216
self.assertEqual(command_result.process.returncode, 0)
@@ -1879,6 +1924,74 @@ def _verify_build_succeeds(self, build_dir):
18791924
self.assertIn("BuildImageFunction", build_dir_files)
18801925

18811926

1927+
@skipIf(SKIP_DOCKER_TESTS, SKIP_DOCKER_MESSAGE)
1928+
@parameterized_class(
1929+
("cached", "parallel", "use_custom_build_dir"),
1930+
[
1931+
(False, False, False), # Basic
1932+
(True, False, False), # With Caching
1933+
(False, True, False), # With parallelism
1934+
(False, False, True), # With custom build dir
1935+
],
1936+
)
1937+
@pytest.mark.filterwarnings("ignore::ResourceWarning")
1938+
@pytest.mark.tier1
1939+
class TestBuildImageWithBuildkit(BuildIntegBase):
1940+
"""Test building image functions with buildkit"""
1941+
1942+
template = "template_image.yaml"
1943+
function_logical_id = "ImageFunction"
1944+
1945+
def test_build_image_function_with_buildkit(self):
1946+
client = ContainerClientFactory.create_client()
1947+
is_available, error_msg = CLIBuildClient.is_available(client.get_runtime_type())
1948+
1949+
if not is_available:
1950+
self.skipTest(f"Buildkit not available: {error_msg}")
1951+
1952+
build_dir = self.custom_build_dir if self.use_custom_build_dir else None
1953+
1954+
tag = uuid4().hex
1955+
overrides = {
1956+
"Runtime": "3.12",
1957+
"Handler": "main.handler",
1958+
"DockerFile": "Dockerfile",
1959+
"Tag": tag,
1960+
}
1961+
cmdlist = self.get_command_list(
1962+
parameter_overrides=overrides,
1963+
use_buildkit=True,
1964+
cached=self.cached,
1965+
parallel=self.parallel,
1966+
build_dir=build_dir,
1967+
)
1968+
1969+
command_result = run_command(cmdlist, cwd=self.working_dir)
1970+
self.assertEqual(command_result.process.returncode, 0)
1971+
1972+
if self.use_custom_build_dir:
1973+
built_template = Path(self.custom_build_dir, "template.yaml")
1974+
else:
1975+
built_template = self.built_template
1976+
1977+
# Verify image was built
1978+
self._verify_image_build_artifact(
1979+
built_template,
1980+
self.function_logical_id,
1981+
"ImageUri",
1982+
f"{self.function_logical_id.lower()}:{tag}",
1983+
)
1984+
1985+
# Verify image works
1986+
expected = {"pi": "3.14"}
1987+
self._verify_invoke_built_function(
1988+
built_template,
1989+
self.function_logical_id,
1990+
self._make_parameter_override_arg(overrides),
1991+
expected,
1992+
)
1993+
1994+
18821995
@parameterized_class(
18831996
("template", "stack_paths", "layer_full_path", "function_full_paths", "invoke_error_message"),
18841997
[

tests/setup_finch.sh

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ sudo apt-get autoremove -y || true
1515

1616
echo "=== Installing Finch ==="
1717
for i in {1..3}; do
18-
if curl -fsSL https://artifact.runfinch.com/deb/GPG_KEY.pub | sudo gpg --dearmor -o /usr/share/keyrings/runfinch-finch-archive-keyring.gpg; then
19-
break
20-
fi
21-
sleep 10
18+
if curl -fsSL https://artifact.runfinch.com/deb/GPG_KEY.pub | sudo gpg --dearmor -o /usr/share/keyrings/runfinch-finch-archive-keyring.gpg; then
19+
break
20+
fi
21+
sleep 10
2222
done
2323

2424
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
3131
sleep 3
3232
sudo chmod 666 /var/run/finch.sock
3333

34+
echo "=== Configuring finch for non-root access ==="
35+
sudo chmod +s /usr/libexec/finch/nerdctl
36+
sudo chmod +s /usr/bin/finch
37+
3438
echo "=== Waiting for Finch to be ready ==="
3539
for i in {1..12}; do
36-
if sudo finch info >/dev/null 2>&1; then
37-
break
38-
fi
39-
sleep 5
40+
if sudo finch info >/dev/null 2>&1; then
41+
break
42+
fi
43+
sleep 5
4044
done
4145

4246
echo "=== Configuring buildkit sockets ==="
@@ -52,4 +56,5 @@ sudo finch run --privileged --rm tonistiigi/binfmt:master --install all
5256

5357
echo "=== Finch setup complete ==="
5458
sudo finch info
55-
sudo finch version
59+
# Run finch without sudo here to confirm that it's not required
60+
finch version

0 commit comments

Comments
 (0)