Skip to content

Commit 3c74e60

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 0a72487 commit 3c74e60

File tree

15 files changed

+288
-1
lines changed

15 files changed

+288
-1
lines changed

src/buildstream/_frontend/cli.py

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

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

src/buildstream/_frontend/widget.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,20 @@ def show_pipeline(self, dependencies, format_):
461461
# Dump the SourceInfo provenance objects in yaml format
462462
line = p.fmt_subst(line, "source-info", _yaml.roundtrip_dump_string(all_source_infos))
463463

464+
# Artifact CAS Digest
465+
if "%{artifact-cas-digest" in format_:
466+
artifact = element._get_artifact()
467+
if artifact.cached():
468+
artifact_files = artifact.get_files()
469+
# We call the private CasBasedDirectory._get_digest() for
470+
# the moment, we should make it public on Directory.
471+
artifact_digest = artifact_files._get_digest()
472+
formated_artifact_digest = "{}/{}".format(artifact_digest.hash, artifact_digest.size_bytes)
473+
line = p.fmt_subst(line, "artifact-cas-digest", formated_artifact_digest)
474+
else:
475+
line = p.fmt_subst(line, "artifact-cas-digest", "")
476+
element.warn("Cannot obtain CAS digest because artifact is not cached")
477+
464478
report += line + "\n"
465479

466480
return report.rstrip("\n")
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
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+
20+
import pytest
21+
22+
from buildstream._testing import cli # pylint: disable=unused-import
23+
24+
from tests.testutils import (
25+
create_artifact_share,
26+
assert_shared,
27+
assert_not_shared,
28+
)
29+
30+
31+
DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "show_artifact_cas_digest_project")
32+
33+
34+
# This tests that a target that hasn't been built locally
35+
# and that isn't cached remotely has not artifact CAS
36+
# digest.
37+
#
38+
# The test is performed without a remote cache.
39+
#
40+
@pytest.mark.datafiles(DATA_DIR)
41+
@pytest.mark.parametrize(
42+
"target",
43+
[
44+
"import-basic-files.bst",
45+
"import-executable-files.bst",
46+
"import-symlinks.bst",
47+
],
48+
ids=["basic-files", "executable-files", "symlinks"],
49+
)
50+
def test_show_artifact_cas_digest_uncached(cli, tmpdir, datafiles, target):
51+
project = str(datafiles)
52+
expected_no_digest = ""
53+
54+
# Check the target has not been built locally and is not existing in the remote cache
55+
assert (
56+
# May be "buildable" or "waiting" but shouldn't be "cached"
57+
cli.get_element_state(project, target) != "cached"
58+
)
59+
60+
# Check the target has no artifact digest
61+
result = cli.run(project=project, silent=True, args=["show", "--format", "%{name},%{artifact-cas-digest}", target])
62+
result.assert_success()
63+
64+
received_digest = result.output.splitlines()[0]
65+
assert received_digest == "{target},{digest}".format(target=target, digest=expected_no_digest)
66+
67+
68+
# This tests that a target that has been built locally and
69+
# with no remote has an artifact CAS digest.
70+
#
71+
# The test is performed without a remote cache.
72+
#
73+
@pytest.mark.datafiles(DATA_DIR)
74+
@pytest.mark.parametrize(
75+
"target, expected_digest",
76+
[
77+
("import-basic-files.bst", "7093d3c89029932ce1518bd2192e1d3cf60fd88e356b39195d10b87b598c78f0/168"),
78+
("import-executable-files.bst", "133a9ae2eda30945a363272ac14bb2c8a941770b5a37c2847c99934f2972ce4f/170"),
79+
("import-symlinks.bst", "95947ea55021e26cec4fd4f9de90d2b7f4f7d803ccc91656b3e1f2c9923ddf19/131"),
80+
],
81+
ids=["basic-files", "executable-files", "symlinks"],
82+
)
83+
def test_show_artifact_cas_digest_cached(cli, tmpdir, datafiles, target, expected_digest):
84+
project = str(datafiles)
85+
86+
# Build the target locally
87+
result = cli.run(project=project, silent=True, args=["build", target])
88+
result.assert_success()
89+
90+
# Check the target has been built locally and is existing in the remote cache
91+
assert cli.get_element_state(project, target) == "cached"
92+
93+
# Check the target has an artifact digest
94+
result = cli.run(project=project, silent=True, args=["show", "--format", "%{name},%{artifact-cas-digest}", target])
95+
result.assert_success()
96+
97+
received_digest = result.output.splitlines()[0]
98+
assert received_digest == "{target},{digest}".format(target=target, digest=expected_digest)
99+
100+
101+
# This tests that the target and its dependencies are built
102+
# as expected and that all their artifact CAS digests are
103+
# available.
104+
#
105+
# The test is performed without a remote cache.
106+
#
107+
@pytest.mark.datafiles(DATA_DIR)
108+
@pytest.mark.parametrize(
109+
"target, expected_digests",
110+
[
111+
(
112+
"dependencies.bst",
113+
{
114+
"dependencies.bst": "7093d3c89029932ce1518bd2192e1d3cf60fd88e356b39195d10b87b598c78f0/168",
115+
"import-symlinks.bst": "95947ea55021e26cec4fd4f9de90d2b7f4f7d803ccc91656b3e1f2c9923ddf19/131",
116+
}
117+
),
118+
],
119+
ids=["dependencies"],
120+
)
121+
def test_show_artifact_cas_digest_dependencies(cli, tmpdir, datafiles, target, expected_digests):
122+
project = str(datafiles)
123+
expected_no_digest = ""
124+
125+
# Check the target and its dependencies have not been built locally
126+
for component in sorted(expected_digests.keys()):
127+
assert (
128+
# May be "buildable" or "waiting" but shouldn't be "cached"
129+
cli.get_element_state(project, component) != "cached"
130+
)
131+
132+
# Check the target and its dependencies have no artifact digest
133+
result = cli.run(project=project, silent=True, args=["show", "--format", "%{name},%{artifact-cas-digest}", target])
134+
result.assert_success()
135+
136+
digests = dict(line.split(",", 2) for line in result.output.splitlines())
137+
assert len(digests) == len(expected_digests)
138+
139+
for component, received in sorted(digests.items()):
140+
assert received == expected_no_digest
141+
142+
# Build the target and its dependencies locally
143+
result = cli.run(project=project, silent=True, args=["build", target])
144+
result.assert_success()
145+
146+
# Check the target and its dependencies have been built locally
147+
for component in sorted(expected_digests.keys()):
148+
assert cli.get_element_state(project, component) == "cached"
149+
150+
# Check the target and its dependencies have an artifact digest
151+
result = cli.run(project=project, silent=True, args=["show", "--format", "%{name},%{artifact-cas-digest}", target])
152+
result.assert_success()
153+
154+
digests = dict(line.split(",", 2) for line in result.output.splitlines())
155+
assert len(digests) == len(expected_digests)
156+
157+
for component, received in sorted(digests.items()):
158+
assert received == expected_digests[component]
159+
160+
161+
# This tests:
162+
# - that a target that hasn't been built locally and that
163+
# isn't cached remotely has no artifact CAS digest,
164+
# - that a target that has been built locally and that is
165+
# cached remotely has an artifact CAS digest,
166+
# - that a target that hasn't been built locally and that
167+
# is cached remotely has no artifact CAS digest.
168+
#
169+
# The test is performed with a remote cache, multiple tests
170+
# are performed at once and on a single element because
171+
# setting up a share is expensive.
172+
#
173+
@pytest.mark.datafiles(DATA_DIR)
174+
def test_show_artifact_cas_digest_remote(cli, tmpdir, datafiles):
175+
project = str(datafiles)
176+
target = "import-basic-files.bst"
177+
expected_no_digest = ""
178+
179+
# Configure a local cache
180+
local_cache = os.path.join(str(tmpdir), "cache")
181+
cli.configure({"cachedir": local_cache})
182+
183+
with create_artifact_share(os.path.join(str(tmpdir), "artifactshare")) as share:
184+
185+
cli.configure({"artifacts": {"servers": [{"url": share.repo, "push": True}]}})
186+
187+
# Test a target cached neither locally or remotely has no digest
188+
189+
# Check the target has not been built locally and is not existing in the remote cache
190+
assert cli.get_element_state(project, target) == "buildable"
191+
assert_not_shared(cli, share, project, target)
192+
193+
# Check the target has no artifact digest
194+
result = cli.run(
195+
project=project, silent=True, args=["show", "--format", "%{name},%{artifact-cas-digest}", target]
196+
)
197+
result.assert_success()
198+
199+
received_digest = result.output.splitlines()[0]
200+
assert received_digest == "{target},{digest}".format(target=target, digest=expected_no_digest)
201+
202+
# Test a target cached locally has a digest
203+
204+
# Build the target locally and cache it remotely
205+
result = cli.run(project=project, silent=True, args=["build", target])
206+
result.assert_success()
207+
208+
# Check the target has been built and shared
209+
assert cli.get_element_state(project, target) == "cached"
210+
assert_shared(cli, share, project, target)
211+
212+
# Check the target has an artifact digest
213+
result = cli.run(
214+
project=project, silent=True, args=["show", "--format", "%{name},%{artifact-cas-digest}", target]
215+
)
216+
result.assert_success()
217+
218+
received_digest = result.output.splitlines()[0]
219+
assert received_digest != "{target},{digest}".format(target=target, digest=expected_no_digest)
220+
221+
# Test a target cached remotely but not locally has no digest
222+
223+
# Delete the locally cached target
224+
result = cli.run(project=project, silent=True, args=["artifact", "delete", target])
225+
result.assert_success()
226+
227+
# Check the target has been deleted locally but not remotely
228+
assert cli.get_element_state(project, target) == "buildable"
229+
assert_shared(cli, share, project, target)
230+
231+
# Check the target has an artifact digest
232+
result = cli.run(
233+
project=project, silent=True, args=["show", "--format", "%{name},%{artifact-cas-digest}", target]
234+
)
235+
result.assert_success()
236+
237+
received_digest = result.output.splitlines()[0]
238+
assert received_digest == "{target},{digest}".format(target=target, digest=expected_no_digest)
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.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
file contents

0 commit comments

Comments
 (0)