blob: 8bf193a19838c352d19f54d26965e2066000d4ef [file] [log] [blame]
#
# Copyright (C) 2018 Codethink Limited
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
#
# Authors: Tristan Maat <tristan.maat@codethink.co.uk>
#
# Pylint doesn't play well with fixtures and dependency injection from pytest
# pylint: disable=redefined-outer-name
import os
import re
from unittest import mock
import pytest
from buildstream._exceptions import ErrorDomain, LoadErrorReason
from buildstream.testing import cli # pylint: disable=unused-import
from tests.testutils import create_element_size, update_element_size, wait_for_cache_granularity
DATA_DIR = os.path.join(
os.path.dirname(os.path.realpath(__file__)),
"expiry"
)
# Ensure that the cache successfully removes an old artifact if we do
# not have enough space left.
@pytest.mark.datafiles(DATA_DIR)
def test_artifact_expires(cli, datafiles):
project = str(datafiles)
element_path = 'elements'
cli.configure({
'cache': {
'quota': 10000000,
}
})
# Create an element that uses almost the entire cache (an empty
# ostree cache starts at about ~10KiB, so we need a bit of a
# buffer)
create_element_size('target.bst', project, element_path, [], 6000000)
res = cli.run(project=project, args=['build', 'target.bst'])
res.assert_success()
assert cli.get_element_state(project, 'target.bst') == 'cached'
# Our cache should now be almost full. Let's create another
# artifact and see if we can cause buildstream to delete the old
# one.
create_element_size('target2.bst', project, element_path, [], 6000000)
res = cli.run(project=project, args=['build', 'target2.bst'])
res.assert_success()
# Check that the correct element remains in the cache
states = cli.get_element_states(project, ['target.bst', 'target2.bst'])
assert states['target.bst'] != 'cached'
assert states['target2.bst'] == 'cached'
# Ensure that we don't end up deleting the whole cache (or worse) if
# we try to store an artifact that is too large to fit in the quota.
@pytest.mark.parametrize('size', [
# Test an artifact that is obviously too large
(500000),
# Test an artifact that might be too large due to slight overhead
# of storing stuff in ostree
(399999)
])
@pytest.mark.datafiles(DATA_DIR)
def test_artifact_too_large(cli, datafiles, size):
project = str(datafiles)
element_path = 'elements'
cli.configure({
'cache': {
'quota': 400000
}
})
# Create an element whose artifact is too large
create_element_size('target.bst', project, element_path, [], size)
res = cli.run(project=project, args=['build', 'target.bst'])
res.assert_main_error(ErrorDomain.STREAM, None)
res.assert_task_error(ErrorDomain.CAS, 'cache-too-full')
@pytest.mark.datafiles(DATA_DIR)
def test_expiry_order(cli, datafiles):
project = str(datafiles)
element_path = 'elements'
checkout = os.path.join(project, 'workspace')
cli.configure({
'cache': {
'quota': 9000000
}
})
# Create an artifact
create_element_size('dep.bst', project, element_path, [], 2000000)
res = cli.run(project=project, args=['build', 'dep.bst'])
res.assert_success()
# Create another artifact
create_element_size('unrelated.bst', project, element_path, [], 2000000)
res = cli.run(project=project, args=['build', 'unrelated.bst'])
res.assert_success()
# And build something else
create_element_size('target.bst', project, element_path, [], 2000000)
res = cli.run(project=project, args=['build', 'target.bst'])
res.assert_success()
create_element_size('target2.bst', project, element_path, [], 2000000)
res = cli.run(project=project, args=['build', 'target2.bst'])
res.assert_success()
wait_for_cache_granularity()
# Now extract dep.bst
res = cli.run(project=project, args=['artifact', 'checkout', 'dep.bst', '--directory', checkout])
res.assert_success()
# Finally, build something that will cause the cache to overflow
create_element_size('expire.bst', project, element_path, [], 2000000)
res = cli.run(project=project, args=['build', 'expire.bst'])
res.assert_success()
# While dep.bst was the first element to be created, it should not
# have been removed.
# Note that buildstream will reduce the cache to 50% of the
# original size - we therefore remove multiple elements.
check_elements = [
'unrelated.bst', 'target.bst', 'target2.bst', 'dep.bst', 'expire.bst'
]
states = cli.get_element_states(project, check_elements)
assert (tuple(states[element] for element in check_elements) ==
('buildable', 'buildable', 'buildable', 'cached', 'cached', ))
# Ensure that we don't accidentally remove an artifact from something
# in the current build pipeline, because that would be embarassing,
# wouldn't it?
@pytest.mark.datafiles(DATA_DIR)
def test_keep_dependencies(cli, datafiles):
project = str(datafiles)
element_path = 'elements'
cli.configure({
'cache': {
'quota': 10000000
}
})
# Create a pretty big dependency
create_element_size('dependency.bst', project, element_path, [], 5000000)
res = cli.run(project=project, args=['build', 'dependency.bst'])
res.assert_success()
# Now create some other unrelated artifact
create_element_size('unrelated.bst', project, element_path, [], 4000000)
res = cli.run(project=project, args=['build', 'unrelated.bst'])
res.assert_success()
# Check that the correct element remains in the cache
states = cli.get_element_states(project, ['dependency.bst', 'unrelated.bst'])
assert states['dependency.bst'] == 'cached'
assert states['unrelated.bst'] == 'cached'
# We try to build an element which depends on the LRU artifact,
# and could therefore fail if we didn't make sure dependencies
# aren't removed.
#
# Since some artifact caches may implement weak cache keys by
# duplicating artifacts (bad!) we need to make this equal in size
# or smaller than half the size of its dependencies.
#
create_element_size('target.bst', project,
element_path, ['dependency.bst'], 2000000)
res = cli.run(project=project, args=['build', 'target.bst'])
res.assert_success()
states = cli.get_element_states(project, ['target.bst', 'unrelated.bst'])
assert states['target.bst'] == 'cached'
assert states['dependency.bst'] == 'cached'
assert states['unrelated.bst'] != 'cached'
# Assert that we never delete a dependency required for a build tree
@pytest.mark.datafiles(DATA_DIR)
def test_never_delete_required(cli, datafiles):
project = str(datafiles)
element_path = 'elements'
cli.configure({
'cache': {
'quota': 10000000
},
'scheduler': {
'builders': 1
}
})
# Create a linear build tree
create_element_size('dep1.bst', project, element_path, [], 8000000)
create_element_size('dep2.bst', project, element_path, ['dep1.bst'], 8000000)
create_element_size('dep3.bst', project, element_path, ['dep2.bst'], 8000000)
create_element_size('target.bst', project, element_path, ['dep3.bst'], 8000000)
# I dont know why building dep2.bst fixis a issue with file systems that only
# recored mtimes to 1 seconds resolution
# TODO fix bug that causes slow filesystems to need this line and then
# remove this line
res = cli.run(project=project, args=['build', 'dep2.bst'])
# We try to build this pipeline, but it's too big for the
# cache. Since all elements are required, the build should fail.
res = cli.run(project=project, args=['build', 'target.bst'])
res.assert_main_error(ErrorDomain.STREAM, None)
res.assert_task_error(ErrorDomain.CAS, 'cache-too-full')
# Only the first artifact fits in the cache, but we expect
# that the first *two* artifacts will be cached.
#
# This is because after caching the first artifact we must
# proceed to build the next artifact, and we cannot really
# know how large an artifact will be until we try to cache it.
#
# In this case, we deem it more acceptable to not delete an
# artifact which caused the cache to outgrow the quota.
#
# Note that this test only works because we have forced
# the configuration to build one element at a time, in real
# life there may potentially be N-builders cached artifacts
# which exceed the quota
#
states = cli.get_element_states(project, ['target.bst'])
assert states['dep1.bst'] == 'cached'
assert states['dep2.bst'] == 'cached'
assert states['dep3.bst'] != 'cached'
assert states['target.bst'] != 'cached'
# Assert that we never delete a dependency required for a build tree,
# even when the artifact cache was previously populated with
# artifacts we do not require, and the new build is run with dynamic tracking.
#
@pytest.mark.datafiles(DATA_DIR)
def test_never_delete_required_track(cli, datafiles):
project = str(datafiles)
element_path = 'elements'
cli.configure({
'cache': {
'quota': 10000000
},
'scheduler': {
'builders': 1
}
})
# Create a linear build tree
repo_dep1 = create_element_size('dep1.bst', project, element_path, [], 2000000)
repo_dep2 = create_element_size('dep2.bst', project, element_path, ['dep1.bst'], 2000000)
repo_dep3 = create_element_size('dep3.bst', project, element_path, ['dep2.bst'], 2000000)
repo_target = create_element_size('target.bst', project, element_path, ['dep3.bst'], 2000000)
# This should all fit into the artifact cache
res = cli.run(project=project, args=['build', 'target.bst'])
res.assert_success()
# They should all be cached
states = cli.get_element_states(project, ['target.bst'])
assert states['dep1.bst'] == 'cached'
assert states['dep2.bst'] == 'cached'
assert states['dep3.bst'] == 'cached'
assert states['target.bst'] == 'cached'
# Now increase the size of all the elements
#
update_element_size('dep1.bst', project, repo_dep1, 8000000)
update_element_size('dep2.bst', project, repo_dep2, 8000000)
update_element_size('dep3.bst', project, repo_dep3, 8000000)
update_element_size('target.bst', project, repo_target, 8000000)
# Now repeat the same test we did in test_never_delete_required(),
# except this time let's add dynamic tracking
#
res = cli.run(project=project, args=['build', '--track-all', 'target.bst'])
res.assert_main_error(ErrorDomain.STREAM, None)
res.assert_task_error(ErrorDomain.CAS, 'cache-too-full')
# Expect the almost the same result that we did in test_never_delete_required()
# As the source will be downloaded first, we will be over the limit once
# the source for dep2.bst is downloaded
#
states = cli.get_element_states(project, ['target.bst'])
assert states['dep1.bst'] == 'cached'
assert states['dep2.bst'] == 'buildable'
assert states['dep3.bst'] != 'cached'
assert states['target.bst'] != 'cached'
# Ensure that only valid cache quotas make it through the loading
# process.
#
# This test virtualizes the condition to assume a storage volume
# has 10K total disk space, and 6K of it is already in use (not
# including any space used by the artifact cache).
#
# Parameters:
# quota (str): A quota size configuration for the config file
# err_domain (str): An ErrorDomain, or 'success' or 'warning'
# err_reason (str): A reson to compare with an error domain
#
# If err_domain is 'success', then err_reason is unused.
#
# If err_domain is 'warning', then err_reason is asserted to
# be in the stderr.
#
@pytest.mark.parametrize("quota,err_domain,err_reason", [
# Valid configurations
("1", 'success', None),
("1K", 'success', None),
("50%", 'success', None),
("infinity", 'success', None),
("0", 'success', None),
# Invalid configurations
("-1", ErrorDomain.LOAD, LoadErrorReason.INVALID_DATA),
("pony", ErrorDomain.LOAD, LoadErrorReason.INVALID_DATA),
("200%", ErrorDomain.LOAD, LoadErrorReason.INVALID_DATA),
# Not enough space on disk even if you cleaned up
("11K", ErrorDomain.CAS, 'insufficient-storage-for-quota'),
# Not enough space for these caches
("7K", 'warning', 'Your system does not have enough available'),
("70%", 'warning', 'Your system does not have enough available')
])
@pytest.mark.datafiles(DATA_DIR)
def test_invalid_cache_quota(cli, datafiles, quota, err_domain, err_reason):
project = str(datafiles)
os.makedirs(os.path.join(project, 'elements'))
cli.configure({
'cache': {
'quota': quota,
},
})
# We patch how we get space information
# Ideally we would instead create a FUSE device on which we control
# everything.
# If the value is a percentage, we fix the current values to take into
# account the block size, since this is important in how we compute the size
if quota.endswith("%"): # We set the used space at 60% of total space
stats = os.statvfs(".")
free_space = 0.6 * stats.f_bsize * stats.f_blocks
total_space = stats.f_bsize * stats.f_blocks
else:
free_space = 6000
total_space = 10000
volume_space_patch = mock.patch(
"buildstream.utils._get_volume_size",
autospec=True,
return_value=(total_space, free_space),
)
cache_size_patch = mock.patch(
"buildstream._cas.CASQuota.get_cache_size",
autospec=True,
return_value=0,
)
with volume_space_patch, cache_size_patch:
res = cli.run(project=project, args=['workspace', 'list'])
if err_domain == 'success':
res.assert_success()
elif err_domain == 'warning':
assert err_reason in res.stderr
else:
res.assert_main_error(err_domain, err_reason)
# Ensures that when launching BuildStream with a full artifact cache,
# the cache size and cleanup jobs are run before any other jobs.
#
@pytest.mark.datafiles(DATA_DIR)
def test_cleanup_first(cli, datafiles):
project = str(datafiles)
element_path = 'elements'
cli.configure({
'cache': {
'quota': 10000000,
}
})
# Create an element that uses almost the entire cache (an empty
# ostree cache starts at about ~10KiB, so we need a bit of a
# buffer)
create_element_size('target.bst', project, element_path, [], 8000000)
res = cli.run(project=project, args=['build', 'target.bst'])
res.assert_success()
assert cli.get_element_state(project, 'target.bst') == 'cached'
# Now configure with a smaller quota, create a situation
# where the cache must be cleaned up before building anything else.
#
# Fix the fetchers and builders just to ensure a predictable
# sequence of events (although it does not effect this test)
cli.configure({
'cache': {
'quota': 5000000,
},
'scheduler': {
'fetchers': 1,
'builders': 1
}
})
# Our cache is now more than full, BuildStream
create_element_size('target2.bst', project, element_path, [], 4000000)
res = cli.run(project=project, args=['build', 'target2.bst'])
res.assert_success()
# Find all of the activity (like push, pull, src-pull) lines
results = re.findall(r'\[.*\]\[.*\]\[\s*(\S+):.*\]\s*START\s*.*\.log', res.stderr)
# Don't bother checking the order of 'src-pull', it is allowed to start
# before or after the initial cache size job, runs in parallel, and does
# not require ResourceType.CACHE.
results.remove('fetch')
print(results)
# Assert the expected sequence of events
assert results == ['size', 'clean', 'build']
# Check that the correct element remains in the cache
states = cli.get_element_states(project, ['target.bst', 'target2.bst'])
assert states['target.bst'] != 'cached'
assert states['target2.bst'] == 'cached'