richardmaw-codethink pushed to branch richardmaw/artifact-log at BuildStream / buildstream
Commits:
- 
132a5ef2
by Richard Maw at 2018-11-01T16:36:14Z
- 
fc0136c1
by Richard Maw at 2018-11-01T16:36:14Z
- 
d614987b
by Richard Maw at 2018-11-01T16:36:14Z
- 
f667dbe5
by Richard Maw at 2018-11-01T16:36:14Z
- 
e5d02f62
by Richard Maw at 2018-11-01T16:36:14Z
4 changed files:
Changes:
| 1 | +===============
 | |
| 2 | +buildstream 1.4
 | |
| 3 | +===============
 | |
| 4 | + | |
| 5 | +  o Added `bst artifact log` subcommand for viewing build logs.
 | |
| 6 | + | |
| 1 | 7 |  =================
 | 
| 2 | 8 |  buildstream 1.3.1
 | 
| 3 | 9 |  =================
 | 
| ... | ... | @@ -112,6 +112,18 @@ def complete_target(args, incomplete): | 
| 112 | 112 |      return complete_path("File", incomplete, base_directory=base_directory)
 | 
| 113 | 113 |  | 
| 114 | 114 |  | 
| 115 | +def complete_artifact(args, incomplete):
 | |
| 116 | +    from .._context import Context
 | |
| 117 | +    ctx = Context()
 | |
| 118 | + | |
| 119 | +    config = None
 | |
| 120 | +    for i, arg in enumerate(args):
 | |
| 121 | +        if arg in ('-c', '--config'):
 | |
| 122 | +            config = args[i + 1]
 | |
| 123 | +    ctx.load(config)
 | |
| 124 | +    return [ref for ref in ctx.artifactcache.list_artifacts() if ref.startswith(incomplete)]
 | |
| 125 | + | |
| 126 | + | |
| 115 | 127 |  def override_completions(cmd, cmd_param, args, incomplete):
 | 
| 116 | 128 |      """
 | 
| 117 | 129 |      :param cmd_param: command definition
 | 
| ... | ... | @@ -126,13 +138,15 @@ def override_completions(cmd, cmd_param, args, incomplete): | 
| 126 | 138 |      # We can't easily extend click's data structures without
 | 
| 127 | 139 |      # modifying click itself, so just do some weak special casing
 | 
| 128 | 140 |      # right here and select which parameters we want to handle specially.
 | 
| 129 | -    if isinstance(cmd_param.type, click.Path) and \
 | |
| 130 | -       (cmd_param.name == 'elements' or
 | |
| 131 | -        cmd_param.name == 'element' or
 | |
| 132 | -        cmd_param.name == 'except_' or
 | |
| 133 | -        cmd_param.opts == ['--track'] or
 | |
| 134 | -        cmd_param.opts == ['--track-except']):
 | |
| 135 | -        return complete_target(args, incomplete)
 | |
| 141 | +    if isinstance(cmd_param.type, click.Path):
 | |
| 142 | +        if (cmd_param.name == 'elements' or
 | |
| 143 | +                cmd_param.name == 'element' or
 | |
| 144 | +                cmd_param.name == 'except_' or
 | |
| 145 | +                cmd_param.opts == ['--track'] or
 | |
| 146 | +                cmd_param.opts == ['--track-except']):
 | |
| 147 | +            return complete_target(args, incomplete)
 | |
| 148 | +        if cmd_param.name == 'artifacts':
 | |
| 149 | +            return complete_artifact(args, incomplete)
 | |
| 136 | 150 |  | 
| 137 | 151 |      raise CompleteUnhandled()
 | 
| 138 | 152 |  | 
| ... | ... | @@ -829,3 +843,65 @@ def source_bundle(app, element, force, directory, | 
| 829 | 843 |                                   force=force,
 | 
| 830 | 844 |                                   compression=compression,
 | 
| 831 | 845 |                                   except_targets=except_)
 | 
| 846 | + | |
| 847 | + | |
| 848 | +#############################################################
 | |
| 849 | +#                     Artifact Command                      #
 | |
| 850 | +#############################################################
 | |
| 851 | +@cli.group(short_help="Manipulate cached Artifacts")
 | |
| 852 | +def artifact():
 | |
| 853 | +    """Manipulate cached Artifacts"""
 | |
| 854 | +    pass
 | |
| 855 | + | |
| 856 | + | |
| 857 | +################################################################
 | |
| 858 | +#                     Artifact Log Command                     #
 | |
| 859 | +################################################################
 | |
| 860 | +@artifact.command(name='log', short_help="Show logs of an artifact")
 | |
| 861 | +@click.option('-e', '--element', 'elements', help="Show logs for artifacts of this element",
 | |
| 862 | +              type=click.Path(readable=False), multiple=True, required=False)
 | |
| 863 | +@click.argument('artifacts', type=click.Path(), nargs=-1)
 | |
| 864 | +@click.pass_obj
 | |
| 865 | +def artifact_log(app, elements, artifacts):
 | |
| 866 | +    """Show logs of all artifacts"""
 | |
| 867 | +    from tempfile import TemporaryDirectory
 | |
| 868 | +    from .._exceptions import ArtifactError
 | |
| 869 | +    from .._message import MessageType
 | |
| 870 | +    from .._pipeline import PipelineSelection
 | |
| 871 | + | |
| 872 | +    with app.initialized(), TemporaryDirectory(dir=os.path.join(app.context.artifactdir, 'tmp')) as td:
 | |
| 873 | +        cache = app.context.artifactcache
 | |
| 874 | + | |
| 875 | +        refs = []
 | |
| 876 | +        if artifacts:
 | |
| 877 | +            for artifact in artifacts:
 | |
| 878 | +                try:
 | |
| 879 | +                    o = cache.resolve_ref(artifact)
 | |
| 880 | +                except ArtifactError as e:
 | |
| 881 | +                    app._message(MessageType.WARN, "Artifact {} is not cached".format(artifact), detail=str(e))
 | |
| 882 | +                    continue
 | |
| 883 | +                refs.append((artifact, o))
 | |
| 884 | +        if elements:
 | |
| 885 | +            elements = app.stream.load_selection(elements, selection=PipelineSelection.NONE)
 | |
| 886 | +            for element in elements:
 | |
| 887 | +                if not element._cached():
 | |
| 888 | +                    app._message(MessageType.WARN, "Element {} is not cached".format(element))
 | |
| 889 | +                    continue
 | |
| 890 | +                ref = cache.get_artifact_fullname(element, element._get_cache_key())
 | |
| 891 | +                o = cache.resolve_ref(ref)
 | |
| 892 | +                refs.append((ref, o))
 | |
| 893 | + | |
| 894 | +        logs = []
 | |
| 895 | +        for ref, o in refs:
 | |
| 896 | +            # TODO: CASCache._get_subdir is a local-private method
 | |
| 897 | +            logsdir = cache._get_subdir(o, "logs")
 | |
| 898 | +            destdir = os.path.join(td, ref)
 | |
| 899 | +            # TODO: CASCache._checkout is a local-private method
 | |
| 900 | +            cache._checkout(destdir, logsdir)
 | |
| 901 | +            logs.extend(os.path.join(destdir, log) for log in os.listdir(destdir))
 | |
| 902 | + | |
| 903 | +        for log in logs:
 | |
| 904 | +            # NOTE: Should click gain the ability to pass files to the pager this can be optimised.
 | |
| 905 | +            with open(log) as f:
 | |
| 906 | +                data = f.read()
 | |
| 907 | +                click.echo_via_pager(data) | 
| ... | ... | @@ -6,6 +6,7 @@ from tests.testutils import cli | 
| 6 | 6 |  DATA_DIR = os.path.dirname(os.path.realpath(__file__))
 | 
| 7 | 7 |  | 
| 8 | 8 |  MAIN_COMMANDS = [
 | 
| 9 | +    'artifact ',
 | |
| 9 | 10 |      'build ',
 | 
| 10 | 11 |      'checkout ',
 | 
| 11 | 12 |      'fetch ',
 | 
| 1 | +#
 | |
| 2 | +#  Copyright (C) 2018 Codethink Limited
 | |
| 3 | +#  Copyright (C) 2018 Bloomberg Finance LP
 | |
| 4 | +#
 | |
| 5 | +#  This program is free software; you can redistribute it and/or
 | |
| 6 | +#  modify it under the terms of the GNU Lesser General Public
 | |
| 7 | +#  License as published by the Free Software Foundation; either
 | |
| 8 | +#  version 2 of the License, or (at your option) any later version.
 | |
| 9 | +#
 | |
| 10 | +#  This library is distributed in the hope that it will be useful,
 | |
| 11 | +#  but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
| 12 | +#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 | |
| 13 | +#  Lesser General Public License for more details.
 | |
| 14 | +#
 | |
| 15 | +#  You should have received a copy of the GNU Lesser General Public
 | |
| 16 | +#  License along with this library. If not, see <http://www.gnu.org/licenses/>.
 | |
| 17 | +#
 | |
| 18 | +#  Authors: Richard Maw <richard maw codethink co uk>
 | |
| 19 | +#
 | |
| 20 | + | |
| 21 | +import os
 | |
| 22 | +import pytest
 | |
| 23 | + | |
| 24 | +from tests.testutils import cli_integration as cli
 | |
| 25 | + | |
| 26 | + | |
| 27 | +pytestmark = pytest.mark.integration
 | |
| 28 | + | |
| 29 | + | |
| 30 | +# Project directory
 | |
| 31 | +DATA_DIR = os.path.join(
 | |
| 32 | +    os.path.dirname(os.path.realpath(__file__)),
 | |
| 33 | +    "project",
 | |
| 34 | +)
 | |
| 35 | + | |
| 36 | + | |
| 37 | +@pytest.mark.integration
 | |
| 38 | +@pytest.mark.datafiles(DATA_DIR)
 | |
| 39 | +def test_artifact_log(cli, tmpdir, datafiles):
 | |
| 40 | +    project = os.path.join(datafiles.dirname, datafiles.basename)
 | |
| 41 | + | |
| 42 | +    # Get the cache key of our test element
 | |
| 43 | +    result = cli.run(project=project, silent=True, args=[
 | |
| 44 | +        '--no-colors',
 | |
| 45 | +        'show', '--deps', 'none', '--format', '%{full-key}',
 | |
| 46 | +        'base.bst'
 | |
| 47 | +    ])
 | |
| 48 | +    key = result.output.strip()
 | |
| 49 | + | |
| 50 | +    # Ensure we have an artifact to read
 | |
| 51 | +    result = cli.run(project=project, args=['build', 'base.bst'])
 | |
| 52 | +    assert result.exit_code == 0
 | |
| 53 | + | |
| 54 | +    # Read the log via the element name
 | |
| 55 | +    result = cli.run(project=project, args=['artifact', 'log', '-e', 'base.bst'])
 | |
| 56 | +    assert result.exit_code == 0
 | |
| 57 | +    log = result.output
 | |
| 58 | + | |
| 59 | +    # Read the log via the key
 | |
| 60 | +    result = cli.run(project=project, args=['artifact', 'log', 'test/base/' + key])
 | |
| 61 | +    assert result.exit_code == 0
 | |
| 62 | +    assert log == result.output | 
