Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • willsalmon/buildstream
  • CumHoleZH/buildstream
  • tchaik/buildstream
  • DCotyPortfolio/buildstream
  • jesusoctavioas/buildstream
  • patrickmmartin/buildstream
  • franred/buildstream
  • tintou/buildstream
  • alatiera/buildstream
  • martinblanchard/buildstream
  • neverdie22042524/buildstream
  • Mattlk13/buildstream
  • PServers/buildstream
  • phamnghia610909/buildstream
  • chiaratolentino/buildstream
  • eysz7-x-x/buildstream
  • kerrick1/buildstream
  • matthew-yates/buildstream
  • twofeathers/buildstream
  • mhadjimichael/buildstream
  • pointswaves/buildstream
  • Mr.JackWilson/buildstream
  • Tw3akG33k/buildstream
  • AlexFazakas/buildstream
  • eruidfkiy/buildstream
  • clamotion2/buildstream
  • nanonyme/buildstream
  • wickyjaaa/buildstream
  • nmanchev/buildstream
  • bojorquez.ja/buildstream
  • mostynb/buildstream
  • highpit74/buildstream
  • Demo112/buildstream
  • ba2014sheer/buildstream
  • tonimadrino/buildstream
  • usuario2o/buildstream
  • Angelika123456/buildstream
  • neo355/buildstream
  • corentin-ferlay/buildstream
  • coldtom/buildstream
  • wifitvbox81/buildstream
  • 358253885/buildstream
  • seanborg/buildstream
  • SotK/buildstream
  • DouglasWinship/buildstream
  • karansthr97/buildstream
  • louib/buildstream
  • bwh-ct/buildstream
  • robjh/buildstream
  • we88c0de/buildstream
  • zhengxian5555/buildstream
51 results
Show changes
Commits on Source (4)
  • Qinusty's avatar
    _project.py: Add fatal-warnings configuration item · c2f273d8
    Qinusty authored
    This allows for users to configure fatal-warnings to be either a list of
    warnings or simply True to trigger all configurable warnings as errors.
    
    This commit deprecates the use of fail-on-overlap within project.conf,
    this will now use the fatal-warnings configuration item.
    
    plugin.py: get_warnings() is now available for plugins to override and
    return their configurable warnings.
    
    tests: This modifys the tests/frontend/overlaps.py tests to support the
    new fatal-warnings configuration. Backwards compatibility is also
    tested for `fail-on-overlap`
    
    _versions.py: BST_FORMAT_VERSION bumped to 12 for fatal-warnings
    
    Fixes: #526
    c2f273d8
  • Qinusty's avatar
    git.py: Add configurable warning for inconsistent-submodule · f32311dd
    Qinusty authored
    This follows the implementation of configurable warnings.
    f32311dd
  • Qinusty's avatar
    docs: Add documentation for Configurable Warnings · bfa65db0
    Qinusty authored
    This includes detailing the use of `fatal-warnings` within project.conf
    
    This also adds a deprecation notice to the fail on overlaps section.
    bfa65db0
  • Qinusty's avatar
    tests: Add tests for configurable warnings · 52de7c53
    Qinusty authored
    This adds multiple tests for custom plugin warnings and core warnings,
    providing checks for both cases which should cause warnings and errors
    when configured as fatal.
    52de7c53
Showing
with 446 additions and 24 deletions
......@@ -230,6 +230,12 @@ class App():
# Propagate pipeline feedback to the user
self.context.set_message_handler(self._message_handler)
# Deprecation check now that message handler is initialized
if self.project._fail_on_overlap is not None:
self._message(MessageType.WARN,
"Use of fail-on-overlap within project.conf " +
"is deprecated. Please use fatal-warnings instead.")
# Now that we have a logger and message handler,
# we can override the global exception hook.
sys.excepthook = self._global_exception_handler
......
......@@ -32,6 +32,7 @@ from ._options import OptionPool
from ._artifactcache import ArtifactCache
from ._elementfactory import ElementFactory
from ._sourcefactory import SourceFactory
from .plugin import all_warnings, CoreWarnings
from ._projectrefs import ProjectRefs, ProjectRefStorage
from ._versions import BST_FORMAT_VERSION
......@@ -39,7 +40,6 @@ from ._versions import BST_FORMAT_VERSION
# Project Configuration file
_PROJECT_CONF_FILE = 'project.conf'
# HostMount()
#
# A simple object describing the behavior of
......@@ -85,7 +85,7 @@ class Project():
self.options = None # OptionPool
self.junction = junction # The junction Element object, if this is a subproject
self.fail_on_overlap = False # Whether overlaps are treated as errors
self.ref_storage = None # ProjectRefStorage setting
self.base_variables = {} # The base set of variables
self.base_environment = {} # The base set of environment variables
......@@ -108,6 +108,10 @@ class Project():
self._source_format_versions = {}
self._element_format_versions = {}
self._fail_on_overlap = False # Whether to fail on overlaps or not # Deprecated use _warning_is_fatal()
self._fatal_warnings = [] # A list of warnings which should trigger an error
self._fatal_warnings_provenance = {} # A lookup table for where fatal warnings came from.
self._shell_command = [] # The default interactive shell command
self._shell_environment = {} # Statically set environment vars
self._shell_host_files = [] # A list of HostMount objects
......@@ -232,6 +236,18 @@ class Project():
mirror_list.append(self._aliases[alias])
return mirror_list
# fail_on_overlap
#
# Property added to continue support of Project.fail_on_overlap after
# introduction of fatal-warnings configuration item.
#
# Returns:
# (bool): True if the configuration specifies that overlaps should
# cause errors instead of warnings.
@property
def fail_on_overlap(self):
return self._fail_on_overlap or self._warning_is_fatal(CoreWarnings.OVERLAPS)
# _load():
#
# Loads the project configuration file in the project directory.
......@@ -278,7 +294,7 @@ class Project():
'split-rules', 'elements', 'plugins',
'aliases', 'name',
'artifacts', 'options',
'fail-on-overlap', 'shell',
'fail-on-overlap', 'shell', 'fatal-warnings',
'ref-storage', 'sandbox', 'mirrors',
])
......@@ -404,7 +420,22 @@ class Project():
self._splits = _yaml.node_get(config, Mapping, 'split-rules')
# Fail on overlap
self.fail_on_overlap = _yaml.node_get(config, bool, 'fail-on-overlap')
self._fail_on_overlap = _yaml.node_get(config, bool, 'fail-on-overlap', default_value=None)
# Fatal warnings
p = _yaml.node_get_provenance(config, 'fatal-warnings')
try: # Check for bool type
fatal_warnings = _yaml.node_get(config, bool, 'fatal-warnings', default_value=False)
except (ValueError, LoadError) as e:
try: # Check for list type
fatal_warnings = _yaml.node_get(config, list, 'fatal-warnings', default_value=[])
except (ValueError, LoadError):
raise LoadError(LoadErrorReason.INVALID_DATA,
"{}: Invalid value specified for 'fatal-warnings', ".format(p) +
"must be list or bool.")
# Validate and set fatal warnings
self._set_fatal_warnings(fatal_warnings, p)
# Use separate file for storing source references
self.ref_storage = _yaml.node_get(config, str, 'ref-storage')
......@@ -532,3 +563,60 @@ class Project():
directory = parent_dir
return directory
# _warning_is_fatal():
#
# Returns true if the warning in question should be considered fatal based on
# the project configuration.
#
# Args:
# warning_str (str): The warning configuration string to check against
#
# Returns:
# (bool): True if the warning should be considered fatal and cause an error.
#
def _warning_is_fatal(self, warning_str):
return warning_str in self._fatal_warnings
# _set_fatal_warnings():
#
# Sets self._fatal_warnings appropriately
#
# Args:
# warnings (list|bool): The warnings to set self._fatal_warnings to.
# If True, plugin.ALL_WARNINGS is used. If False, [] is used.
# provenance (str): The provenance assosciated with the warnings parameter.
#
def _set_fatal_warnings(self, warnings, provenance):
if warnings is None:
return
elif isinstance(warnings, bool):
# This can make self._fatal_warnings a reference to all_warnings if warnings is true.
# We can't copy because plugins have not been loaded and their warnings aren't in all_warnings.
self._fatal_warnings = all_warnings if warnings else []
else:
self._fatal_warnings = warnings
# Record the provenance for this configuration for use within _assert_fatal_warnings
self._fatal_warnings_provenance = {warning: provenance for warning in self._fatal_warnings}
# _assert_fatal_warnings():
#
# Checks that the configured fatal-warnings are valid with the currently loaded plugins.
#
# Raises:
# LoadError: When self._fatal_warnings contains warnings not within plugin.all_warnings
def _assert_fatal_warnings(self):
# Check for unknown warnings.
unknown_warnings = list(filter(lambda x: x not in all_warnings, self._fatal_warnings))
if unknown_warnings: # Restrict 'fatal-warnings' to known warnings
provenance = self._fatal_warnings_provenance.get(unknown_warnings[0])
quoted_unknown_warnings = ["'{}'".format(warning) for warning in unknown_warnings]
quoted_all_warnings = ["'{}'".format(warning) for warning in all_warnings]
raise LoadError(LoadErrorReason.INVALID_DATA,
("{}: Invalid {} provided to fatal-warnings ({})\n" +
"Valid options are: ({})")
.format(provenance,
"warning" if len(unknown_warnings) == 1 else "warnings",
", ".join(quoted_unknown_warnings),
", ".join(quoted_all_warnings)))
......@@ -861,6 +861,9 @@ class Stream():
rewritable=rewritable,
fetch_subprojects=fetch_subprojects)
# Ensure configured warnings are available within loaded plugins
self._project._assert_fatal_warnings()
# Hold on to the targets
self.targets = elements
......
......@@ -23,7 +23,7 @@
# This version is bumped whenever enhancements are made
# to the `project.conf` format or the core element format.
#
BST_FORMAT_VERSION = 11
BST_FORMAT_VERSION = 12
# The base BuildStream artifact version
......
......@@ -14,7 +14,15 @@ element-path: .
ref-storage: inline
# Overlaps are just warnings
fail-on-overlap: False
# This has been DEPRECATED in favour of fatal-warnings
#fail-on-overlap: False
# Allows a collection of warnings to be configured to be raised as errors.
# Setting this value to true will enable all possible fatal-warnings
# fatal-warnings: True
# fatal-warnings:
# - overlaps
# Variable Configuration
......
......@@ -47,6 +47,24 @@ it is mandatory to implement the following abstract methods:
Once all configuration has been loaded and preflight checks have passed,
this method is used to inform the core of a plugin's unique configuration.
Configurable Warnings
---------------------
For plugins which wish to provide configurable warnings, it is necessary to override
:func:`Plugin.get_warnings() <buildstream.plugin.Plugin.get_warnings>` to get them registered
with buildstream.
These warnings are used when calling :func:`Plugin.warn() <buildstream.plugin.Plugin.warn>` as an optional
parameter ``warning_token``, this will raise a :class:`PluginError` if the warning is configured as fatal.
Configurable warnings will be prefixed with :func:`Plugin.get_kind() <buildstream.plugin.Plugin.get_kind>`
within buildstream and must be prefixed as such in project configurations. For more detail on project configuration
see :ref:`Configurable Warnings <configurable_warnings>`.
Example
~~~~~~~
If the ``git.py`` plugin made the warning ``"inconsistent-submodule"`` available through
:func:`Plugin.get_warnings() <buildstream.plugin.Plugin.get_warnings>` then it could be referenced in project
configuration as ``"git:inconsistent-submodule"``
Plugin Structure
----------------
......@@ -102,6 +120,20 @@ from . import utils
from ._exceptions import PluginError, ImplError
from ._message import Message, MessageType
# Core Warnings must be prefixed with core:
CORE_WARNING_PREFIX = "core:"
class CoreWarnings():
OVERLAPS = CORE_WARNING_PREFIX + "overlaps"
REF_NOT_IN_TRACK = CORE_WARNING_PREFIX + "ref-not-in-track"
CORE_WARNINGS = [
warning_label
for var, warning_label in CoreWarnings.__dict__.items()
if not var.startswith("__")
]
all_warnings = CORE_WARNINGS + []
class Plugin():
"""Plugin()
......@@ -166,7 +198,7 @@ class Plugin():
# Infer the kind identifier
modulename = type(self).__module__
self.__kind = modulename.split('.')[-1]
_register_warnings(self, self.get_warnings())
self.debug("Created: {}".format(self))
def __del__(self):
......@@ -473,13 +505,21 @@ class Plugin():
"""
self.__message(MessageType.INFO, brief, detail=detail)
def warn(self, brief, *, detail=None):
"""Print a warning message
def warn(self, brief, *, detail=None, warning_token=None):
"""Print a warning message, checks warning_token against project configuration
Args:
brief (str): The brief message
detail (str): An optional detailed message, can be multiline output
warning_token (str): An optional configurable warning assosciated with this warning,
this will cause PluginError to be raised if this warning is configured as fatal.
(*Since 1.4*)
Raises:
(:class:`.PluginError`): When warning_token is considered fatal by the project configuration
"""
if warning_token and self.__warning_is_fatal(_prefix_warning(self, warning_token)):
raise PluginError(message="{}\n{}".format(brief, detail))
self.__message(MessageType.WARN, brief, detail=detail)
def log(self, brief, *, detail=None):
......@@ -606,6 +646,21 @@ class Plugin():
"""
return self.__call(*popenargs, collect_stdout=True, fail=fail, fail_temporarily=fail_temporarily, **kwargs)
def get_warnings(self):
"""Return a collection of configurable warnings
Returns:
(list of str): A list of strings which will extend the configurable warnings collection.
Warnings can be configured as fatal in a project configuration file, any warnings provided here
will be added to the collection used by buildstream.
**Since 1.4**
Plugin implementors should override this method to add their own configurable warnings.
"""
return []
#############################################################
# Private Methods used in BuildStream #
#############################################################
......@@ -708,6 +763,10 @@ class Plugin():
else:
return self.name
def __warning_is_fatal(self, warning):
return self._get_project()._warning_is_fatal(warning)
# Hold on to a lookup table by counter of all instantiated plugins.
# We use this to send the id back from child processes so we can lookup
......@@ -739,6 +798,26 @@ def _plugin_lookup(unique_id):
return __PLUGINS_TABLE[unique_id]
def _prefix_warning(plugin, warning):
if warning.startswith(CORE_WARNING_PREFIX):
return warning
return "{}:{}".format(plugin.get_kind(), warning)
# _register_warnings():
#
# Registers a collection of warnings from a plugin into all_warnings
#
# Args:
# plugin (Plugin): The plugin assosciated with the warnings.
# warnings (list of str): The collection of warnings to prefix and register.
#
def _register_warnings(plugin, warnings):
prefixed_warnings = [
_prefix_warning(plugin, warning)
for warning in warnings
]
all_warnings.extend(prefixed_warnings)
# No need for unregister, WeakValueDictionary() will remove entries
# in itself when the referenced plugins are garbage collected.
def _plugin_register(plugin):
......
......@@ -68,6 +68,12 @@ git - stage files from a git repository
url: upstream:baz.git
checkout: False
**Configurable Warnings:**
This plugin provides the following configurable warnings:
- 'git:inconsistent-submodule' - A submodule was found to be missing from the underlying git repository.
"""
import os
......@@ -84,6 +90,8 @@ from buildstream import utils
GIT_MODULES = '.gitmodules'
# Warnings
INCONSISTENT_SUBMODULE = "inconsistent-submodules"
# Because of handling of submodules, we maintain a GitMirror
# for the primary git source and also for each submodule it
......@@ -283,7 +291,7 @@ class GitMirror(SourceFetcher):
"underlying git repository with `git submodule add`."
self.source.warn("{}: Ignoring inconsistent submodule '{}'"
.format(self.source, submodule), detail=detail)
.format(self.source, submodule), detail=detail, warning_token=INCONSISTENT_SUBMODULE)
return None
......@@ -350,6 +358,9 @@ class GitSource(Source):
return Consistency.RESOLVED
return Consistency.INCONSISTENT
def get_warnings(self):
return [INCONSISTENT_SUBMODULE]
def load_ref(self, node):
self.mirror.ref = self.node_get_member(node, str, 'ref', None)
......
......@@ -126,23 +126,72 @@ following to your ``project.conf``:
The ``ref-storage`` configuration is available since :ref:`format version 8 <project_format_version>`
.. _configurable_warnings:
Configurable Warnings
~~~~~~~~~~~~~~~~~~~~~
Warnings can be configured as fatal using the ``fatal-warnings`` configuration item.
When a warning is configured as fatal, where a warning would usually be thrown instead an error will be thrown
causing the build to fail.
When ``fatal-warnings`` is True, all configurable fatal warnings will be set as fatal. Individual warnings
can also be set by setting ``fatal-warnings`` to a list of warnings.
.. code::
fatal-warnings:
- core:overlaps
- core:ref-not-in-track
- <plugin>:<warning>
Core Configurable warnings include:
- :ref:`core:overlaps <fail_on_overlaps>`
- :ref:`core:ref-not-in-track <ref_not_in_track>`
.. note::
The ``ref-storage`` configuration is available since :ref:`format version 12 <project_format_version>`
.. note::
Other configurable warnings are plugin specific and should be noted within their individual documentation.
.. _fail_on_overlaps:
Fail on overlaps
~~~~~~~~~~~~~~~~
When multiple elements are staged, there's a possibility that different
elements will try and stage different versions of the same file.
When ``fail-on-overlap`` is true, if an overlap is detected
that hasn't been allowed by the element's
:ref:`overlap whitelist<public_overlap_whitelist>`,
then an error will be raised and the build will fail.
.. deprecated:: 1.4
otherwise, a warning will be raised indicating which files had overlaps,
and the order that the elements were overlapped.
When ``fail-on-overlap`` is true, if an overlap is detected
that hasn't been allowed by the element's
:ref:`overlap whitelist<public_overlap_whitelist>`,
then an error will be raised and the build will fail.
Otherwise, a warning will be raised indicating which files had overlaps,
and the order that the elements were overlapped.
.. code:: yaml
# Deprecated
fail-on-overlap: true
.. note::
Since deprecation in :ref:`format version 12 <project_format_version>` the recommended
solution to this is :ref:`Configurable Warnings <configurable_warnings>`
.. _ref_not_in_track:
Ref not in track
~~~~~~~~~~~~~~~~
The configured ref is not valid for the configured track.
.. _project_source_aliases:
......
import pytest
import os
from buildstream.plugin import CoreWarnings
from buildstream._exceptions import ErrorDomain, LoadErrorReason
from buildstream import _yaml
from tests.testutils.runcli import cli
TOP_DIR = os.path.join(
os.path.dirname(os.path.realpath(__file__)),
"configuredwarning"
)
def get_project(fatal_warnings):
return {
"name": "test",
"element-path": "elements",
"plugins": [
{
"origin": "local",
"path": "plugins",
"elements": {
"warninga": 0,
"warningb": 0,
"corewarn": 0,
}
}
],
"fatal-warnings": fatal_warnings
}
def build_project(datafiles, fatal_warnings):
project_path = os.path.join(datafiles.dirname, datafiles.basename)
project = get_project(fatal_warnings)
_yaml.dump(project, os.path.join(project_path, "project.conf"))
return project_path
@pytest.mark.datafiles(TOP_DIR)
@pytest.mark.parametrize("element_name, fatal_warnings, expect_fatal, error_domain", [
("corewarn.bst", [CoreWarnings.OVERLAPS], True, ErrorDomain.STREAM),
("warninga.bst", ["warninga:warning-a"], True, ErrorDomain.STREAM),
("warningb.bst", ["warningb:warning-b"], True, ErrorDomain.STREAM),
("corewarn.bst", [], False, None),
("warninga.bst", [], False, None),
("warningb.bst", [], False, None),
("corewarn.bst", "true", True, ErrorDomain.STREAM),
("warninga.bst", "true", True, ErrorDomain.STREAM),
("warningb.bst", "true", True, ErrorDomain.STREAM),
("warninga.bst", [CoreWarnings.OVERLAPS], False, None),
("warningb.bst", [CoreWarnings.OVERLAPS], False, None),
])
def test_fatal_warnings(cli, datafiles, element_name,
fatal_warnings, expect_fatal, error_domain):
project_path = build_project(datafiles, fatal_warnings)
result = cli.run(project=project_path, args=["build", element_name])
if expect_fatal:
result.assert_main_error(error_domain, None, "Expected fatal execution")
else:
result.assert_success("Unexpected fatal execution")
@pytest.mark.datafiles(TOP_DIR)
def test_invalid_warning(cli, datafiles):
project_path = build_project(datafiles, ["non-existant-warning"])
result = cli.run(project=project_path, args=["build", "corewarn.bst"])
result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.INVALID_DATA)
kind: corewarn
\ No newline at end of file
kind: warninga
kind: warningb
from buildstream import Element
from buildstream.plugin import CoreWarnings
class CoreWarn(Element):
def configure(self, node):
pass
def preflight(self):
pass
def get_unique_key(self):
pass
def get_warnings(self):
return [] # CoreWarnings should be included regardless of plugins.
def configure_sandbox(self, sandbox):
pass
def stage(self, sandbox):
pass
def assemble(self, sandbox):
self.warn("Testing: CoreWarning produced during assemble",
warning_token=CoreWarnings.OVERLAPS)
def setup():
return CoreWarn
\ No newline at end of file
from buildstream import Element
WARNING_A = "warning-a"
class WarningA(Element):
def configure(self, node):
pass
def preflight(self):
pass
def get_unique_key(self):
pass
def get_warnings(self):
return [WARNING_A]
def configure_sandbox(self, sandbox):
pass
def stage(self, sandbox):
pass
def assemble(self, sandbox):
self.warn("Testing: warning-a produced during assemble", warning_token=WARNING_A)
def setup():
return WarningA
\ No newline at end of file
from buildstream import Element
WARNING_B = "warning-b"
class WarningB(Element):
def configure(self, node):
pass
def preflight(self):
pass
def get_unique_key(self):
pass
def get_warnings(self):
return [WARNING_B]
def configure_sandbox(self, sandbox):
pass
def stage(self, sandbox):
pass
def assemble(self, sandbox):
self.warn("Testing: warning-b produced during assemble", warning_token=WARNING_B)
def setup():
return WarningB
\ No newline at end of file
name: test
element-path: elements
plugins:
- origin: local
path: element_plugins
elements:
warninga: 0
warningb: 0
......@@ -3,6 +3,7 @@ import pytest
from tests.testutils.runcli import cli
from buildstream._exceptions import ErrorDomain
from buildstream import _yaml
from buildstream.plugin import CoreWarnings
# Project directory
DATA_DIR = os.path.join(
......@@ -16,26 +17,31 @@ project_template = {
}
def gen_project(project_dir, fail_on_overlap):
def gen_project(project_dir, fail_on_overlap, use_fatal_warnings=True):
template = dict(project_template)
template["fail-on-overlap"] = fail_on_overlap
if use_fatal_warnings:
template["fatal-warnings"] = [CoreWarnings.OVERLAPS] if fail_on_overlap else []
else:
template["fail-on-overlap"] = fail_on_overlap
projectfile = os.path.join(project_dir, "project.conf")
_yaml.dump(template, projectfile)
@pytest.mark.datafiles(DATA_DIR)
def test_overlaps(cli, datafiles):
@pytest.mark.parametrize("use_fatal_warnings", [True, False])
def test_overlaps(cli, datafiles, use_fatal_warnings):
project_dir = str(datafiles)
gen_project(project_dir, False)
gen_project(project_dir, False, use_fatal_warnings)
result = cli.run(project=project_dir, silent=True, args=[
'build', 'collect.bst'])
result.assert_success()
@pytest.mark.datafiles(DATA_DIR)
def test_overlaps_error(cli, datafiles):
@pytest.mark.parametrize("use_fatal_warnings", [True, False])
def test_overlaps_error(cli, datafiles, use_fatal_warnings):
project_dir = str(datafiles)
gen_project(project_dir, True)
gen_project(project_dir, True, use_fatal_warnings)
result = cli.run(project=project_dir, silent=True, args=[
'build', 'collect.bst'])
result.assert_main_error(ErrorDomain.STREAM, None)
......@@ -74,11 +80,12 @@ def test_overlaps_whitelist_on_overlapper(cli, datafiles):
@pytest.mark.datafiles(DATA_DIR)
def test_overlaps_script(cli, datafiles):
@pytest.mark.parametrize("use_fatal_warnings", [True, False])
def test_overlaps_script(cli, datafiles, use_fatal_warnings):
# Test overlaps with script element to test
# Element.stage_dependency_artifacts() with Scope.RUN
project_dir = str(datafiles)
gen_project(project_dir, False)
gen_project(project_dir, False, use_fatal_warnings)
result = cli.run(project=project_dir, silent=True, args=[
'build', 'script.bst'])
result.assert_success()