Skip to content

Commit d2a6b91

Browse files
author
Adrien Plazas
committed
_frontend/widget: Add artifact-cas-digest show format
This adds the artifact-cas-digest format string to the show command, allowing to show the CAS digest of the built artifact.
1 parent 6c06e1b commit d2a6b91

File tree

16 files changed

+303
-1
lines changed

16 files changed

+303
-1
lines changed

man/bst-show.1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Show elements in the pipeline
4545
%{deps} A list of all dependencies
4646
%{build-deps} A list of build dependencies
4747
%{runtime-deps} A list of runtime dependencies
48+
%{artifact-cas-digest} The CAS digest of the built artifact
4849
.PP
4950
The value of the %{symbol} without the leading '%' character is understood
5051
as a pythonic formatting string, so python formatting features apply,

src/buildstream/_frontend/cli.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,7 @@ def show(app, elements, deps, except_, order, format_):
613613
%{deps} A list of all dependencies
614614
%{build-deps} A list of build dependencies
615615
%{runtime-deps} A list of runtime dependencies
616+
%{artifact-cas-digest} The CAS digest of the built artifact
616617
617618
The value of the %{symbol} without the leading '%' character is understood
618619
as a pythonic formatting string, so python formatting features apply,
@@ -638,7 +639,8 @@ def show(app, elements, deps, except_, order, format_):
638639
state_match = re.search(r"%(\{(state)[^%]*?\})", format_)
639640
key_match = re.search(r"%(\{(key)[^%]*?\})", format_)
640641
full_key_match = re.search(r"%(\{(full-key)[^%]*?\})", format_)
641-
need_state = bool(state_match or key_match or full_key_match)
642+
artifact_cas_digest_match = re.search(r"%(\{(artifact-cas-digest)[^%]*?\})", format_)
643+
need_state = bool(state_match or key_match or full_key_match or artifact_cas_digest_match)
642644

643645
if not elements:
644646
elements = app.project.get_default_targets()

src/buildstream/_frontend/widget.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,25 @@ def show_pipeline(self, dependencies, format_):
437437
runtime_deps = [e._get_full_name() for e in element._dependencies(_Scope.RUN, recurse=False)]
438438
line = p.fmt_subst(line, "runtime-deps", _yaml.roundtrip_dump_string(runtime_deps).rstrip("\n"))
439439

440+
# Artifact CAS Digest
441+
if "%{artifact-cas-digest" in format_:
442+
artifact = element._get_artifact()
443+
if not artifact.query_cache():
444+
artifact = None
445+
if artifact is not None:
446+
artifact_files = artifact.get_files()
447+
# We call the private CasBasedDirectory._get_digest() for
448+
# the moment, we should make it public on Directory.
449+
artifact_digest = artifact_files._get_digest()
450+
formated_artifact_digest = "{}/{}".format(artifact_digest.hash, artifact_digest.size_bytes)
451+
line = p.fmt_subst(line, "artifact-cas-digest", formated_artifact_digest)
452+
else:
453+
# FIXME
454+
# We could instead collect all of the elements that are not
455+
# cached and issue a single warning message.
456+
line = p.fmt_subst(line, "artifact-cas-digest", "")
457+
element.warn("Cannot obtain CAS digest because artifact is not cached")
458+
440459
report += line + "\n"
441460

442461
return report.rstrip("\n")
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
#
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
#
14+
15+
# Pylint doesn't play well with fixtures and dependency injection from pytest
16+
# pylint: disable=redefined-outer-name
17+
18+
import os
19+
import shutil
20+
21+
import pytest
22+
23+
from buildstream._testing import cli # pylint: disable=unused-import
24+
from buildstream.exceptions import ErrorDomain
25+
26+
from tests.testutils import (
27+
create_artifact_share,
28+
assert_shared,
29+
assert_not_shared,
30+
)
31+
32+
33+
DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "show_artifact_cas_digest_project")
34+
35+
36+
# This tests that a target that hasn't been built locally
37+
# and that isn't cached remotely has not artifact CAS
38+
# digest.
39+
#
40+
# The test is performed without a remote cache.
41+
#
42+
@pytest.mark.datafiles(DATA_DIR)
43+
@pytest.mark.parametrize(
44+
"target",
45+
[
46+
"import-basic-files.bst",
47+
"import-executable-files.bst",
48+
"import-symlinks.bst",
49+
],
50+
ids=["basic-files", "executable-files", "symlinks"],
51+
)
52+
def test_show_artifact_cas_digest_uncached_no_remote(cli, tmpdir, datafiles, target):
53+
project = str(datafiles)
54+
expected_no_digest = ""
55+
56+
# Check the target has not been built locally and is not existing in the remote cache
57+
assert cli.get_element_state(project, target) != "cached" # May be "buildable" or "waiting" but shouldn't be "cached"
58+
59+
# Check the target has no artifact digest
60+
result = cli.run(project=project, silent=True, args=["show", "--format", "%{name},%{artifact-cas-digest}", target])
61+
result.assert_success()
62+
63+
received_digest = result.output.splitlines()[0]
64+
assert received_digest == "{target},{digest}".format(target=target, digest=expected_no_digest)
65+
66+
67+
# This tests that a target that hasn't been built locally
68+
# and that isn't cached remotely has no artifact CAS
69+
# digest.
70+
#
71+
# The test is performed with a remote cache.
72+
#
73+
@pytest.mark.datafiles(DATA_DIR)
74+
@pytest.mark.parametrize(
75+
"target",
76+
[
77+
"import-basic-files.bst",
78+
"import-executable-files.bst",
79+
"import-symlinks.bst",
80+
],
81+
ids=["basic-files", "executable-files", "symlinks"],
82+
)
83+
def test_show_artifact_cas_digest_uncached(cli, tmpdir, datafiles, target):
84+
project = str(datafiles)
85+
expected_no_digest = ""
86+
87+
# Configure a local cache
88+
local_cache = os.path.join(str(tmpdir), "cache")
89+
cli.configure({"cachedir": local_cache})
90+
91+
with create_artifact_share(os.path.join(str(tmpdir), "artifactshare")) as share:
92+
93+
cli.configure({"artifacts": {"servers": [{"url": share.repo, "push": True}]}})
94+
95+
# Check the target has not been built locally and is not existing in the remote cache
96+
assert cli.get_element_state(project, target) == "buildable"
97+
assert_not_shared(cli, share, project, target)
98+
99+
# Check the target has no artifact digest
100+
result = cli.run(project=project, silent=True, args=["show", "--format", "%{name},%{artifact-cas-digest}", target])
101+
result.assert_success()
102+
103+
received_digest = result.output.splitlines()[0]
104+
assert received_digest == "{target},{digest}".format(target=target, digest=expected_no_digest)
105+
106+
107+
# This tests that a target that has been built locally and
108+
# that is cached remotely has an artifact CAS digest.
109+
#
110+
# The test is performed with a remote cache.
111+
#
112+
@pytest.mark.datafiles(DATA_DIR)
113+
@pytest.mark.parametrize(
114+
"target, expected_digest",
115+
[
116+
("import-basic-files.bst", "7093d3c89029932ce1518bd2192e1d3cf60fd88e356b39195d10b87b598c78f0/168"),
117+
("import-executable-files.bst", "133a9ae2eda30945a363272ac14bb2c8a941770b5a37c2847c99934f2972ce4f/170"),
118+
("import-symlinks.bst", "95947ea55021e26cec4fd4f9de90d2b7f4f7d803ccc91656b3e1f2c9923ddf19/131"),
119+
],
120+
ids=["basic-files", "executable-files", "symlinks"],
121+
)
122+
def test_show_artifact_cas_digest_cached_remotely(cli, tmpdir, datafiles, target, expected_digest):
123+
project = str(datafiles)
124+
125+
# Configure a local cache
126+
local_cache = os.path.join(str(tmpdir), "cache")
127+
cli.configure({"cachedir": local_cache})
128+
129+
with create_artifact_share(os.path.join(str(tmpdir), "artifactshare")) as share:
130+
131+
cli.configure({"artifacts": {"servers": [{"url": share.repo, "push": True}]}})
132+
133+
# Build the target locally
134+
result = cli.run(project=project, silent=True, args=["build", target])
135+
result.assert_success()
136+
137+
# Check the target has been built locally and is existing in the remote cache
138+
assert cli.get_element_state(project, target) == "cached"
139+
assert_shared(cli, share, project, target)
140+
141+
# Check the target has an artifact digest
142+
result = cli.run(project=project, silent=True, args=["show", "--format", "%{name},%{artifact-cas-digest}", target])
143+
result.assert_success()
144+
145+
received_digest = result.output.splitlines()[0]
146+
assert received_digest == "{target},{digest}".format(target=target, digest=expected_digest)
147+
148+
149+
# This tests that a target that has been built locally and
150+
# that is cached remotely has an artifact CAS digest.
151+
#
152+
# The test is performed with a remote cache.
153+
#
154+
@pytest.mark.datafiles(DATA_DIR)
155+
@pytest.mark.parametrize(
156+
"target",
157+
[
158+
"import-basic-files.bst",
159+
"import-executable-files.bst",
160+
"import-symlinks.bst",
161+
],
162+
ids=["basic-files", "executable-files", "symlinks"],
163+
)
164+
def test_show_artifact_cas_digest_cached_remotely_only(cli, tmpdir, datafiles, target):
165+
project = str(datafiles)
166+
expected_no_digest = ""
167+
168+
# Configure a local cache
169+
local_cache = os.path.join(str(tmpdir), "cache")
170+
cli.configure({"cachedir": local_cache})
171+
172+
with create_artifact_share(os.path.join(str(tmpdir), "artifactshare")) as share:
173+
174+
cli.configure({"artifacts": {"servers": [{"url": share.repo, "push": True}]}})
175+
176+
# Build the target locally
177+
result = cli.run(project=project, silent=True, args=["build", target])
178+
result.assert_success()
179+
180+
# Delete the locally cached target
181+
result = cli.run(project=project, silent=True, args=["artifact", "delete", target])
182+
result.assert_success()
183+
184+
# Check the target has been deleted locally
185+
assert cli.get_element_state(project, target) == "buildable"
186+
assert_shared(cli, share, project, target)
187+
188+
# Check the target has an artifact digest
189+
result = cli.run(project=project, silent=True, args=["show", "--format", "%{name},%{artifact-cas-digest}", target])
190+
result.assert_success()
191+
192+
received_digest = result.output.splitlines()[0]
193+
assert received_digest == "{target},{digest}".format(target=target, digest=expected_no_digest)
194+
195+
196+
# This tests that the target and its dependencies are built
197+
# as expected and that all their artifact CAS digests are
198+
# available.
199+
#
200+
# The test is performed without a remote cache.
201+
#
202+
@pytest.mark.datafiles(DATA_DIR)
203+
@pytest.mark.parametrize(
204+
"target, expected_digests",
205+
[
206+
("dependencies.bst", {
207+
"dependencies.bst": "7093d3c89029932ce1518bd2192e1d3cf60fd88e356b39195d10b87b598c78f0/168",
208+
"import-symlinks.bst": "95947ea55021e26cec4fd4f9de90d2b7f4f7d803ccc91656b3e1f2c9923ddf19/131",
209+
}),
210+
],
211+
ids=["dependencies"],
212+
)
213+
def test_show_artifact_cas_digest_dependencies(cli, tmpdir, datafiles, target, expected_digests):
214+
project = str(datafiles)
215+
expected_no_digest = ""
216+
217+
# Check the target and its dependencies have not been built locally
218+
for component in sorted(expected_digests.keys()):
219+
assert cli.get_element_state(project, component) != "cached" # May be "buildable" or "waiting" but shouldn't be "cached"
220+
221+
# Check the target and its dependencies have no artifact digest
222+
result = cli.run(project=project, silent=True, args=["show", "--format", "%{name},%{artifact-cas-digest}", target])
223+
result.assert_success()
224+
225+
digests = dict(line.split(",", 2) for line in result.output.splitlines())
226+
assert len(digests) == len(expected_digests)
227+
228+
for component, received in sorted(digests.items()):
229+
assert received == expected_no_digest
230+
231+
# Build the target and its dependencies locally
232+
result = cli.run(project=project, silent=True, args=["build", target])
233+
result.assert_success()
234+
235+
# Check the target and its dependencies have been built locally
236+
for component in sorted(expected_digests.keys()):
237+
assert cli.get_element_state(project, component) == "cached"
238+
239+
# Check the target and its dependencies have an artifact digest
240+
result = cli.run(project=project, silent=True, args=["show", "--format", "%{name},%{artifact-cas-digest}", target])
241+
result.assert_success()
242+
243+
digests = dict(line.split(",", 2) for line in result.output.splitlines())
244+
assert len(digests) == len(expected_digests)
245+
246+
for component, received in sorted(digests.items()):
247+
assert received == expected_digests[component]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: import
2+
sources:
3+
- kind: local
4+
path: files/basic-files
5+
depends:
6+
- import-symlinks.bst
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
kind: import
2+
sources:
3+
- kind: local
4+
path: files/basic-files
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
kind: import
2+
sources:
3+
- kind: local
4+
path: files/executable-files
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
kind: import
2+
sources:
3+
- kind: local
4+
path: files/symlinks
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
file contents

tests/frontend/show_artifact_cas_digest_project/files/basic-files/basicfolder/subdir-file

Whitespace-only changes.

0 commit comments

Comments
 (0)