Commit 22d804df authored by Antoine Wacheux's avatar Antoine Wacheux Committed by Tristan Van Berkom
Browse files

Accept the first character as shortcut on interruption prompts

On interruption, this makes buildstream to accept the first character of all
the possible choices as if it was the full command. This behavior has been
added to the failure screen and to the interruption screen.

Fixes #130
parent 380dff80
Loading
Loading
Loading
Loading
Loading
+40 −12
Original line number Original line Diff line number Diff line
@@ -23,6 +23,7 @@ import click
import pkg_resources  # From setuptools
import pkg_resources  # From setuptools
from contextlib import contextmanager
from contextlib import contextmanager
from blessings import Terminal
from blessings import Terminal
from click import UsageError


# Import buildstream public symbols
# Import buildstream public symbols
from .. import Scope, Consistency
from .. import Scope, Consistency
@@ -891,16 +892,16 @@ class App():
            click.echo("\nUser interrupted with ^C\n" +
            click.echo("\nUser interrupted with ^C\n" +
                       "\n"
                       "\n"
                       "Choose one of the following options:\n" +
                       "Choose one of the following options:\n" +
                       "  continue  - Continue queueing jobs as much as possible\n" +
                       "  (c)ontinue  - Continue queueing jobs as much as possible\n" +
                       "  quit      - Exit after all ongoing jobs complete\n" +
                       "  (q)uit      - Exit after all ongoing jobs complete\n" +
                       "  terminate - Terminate any ongoing jobs and exit\n" +
                       "  (t)erminate - Terminate any ongoing jobs and exit\n" +
                       "\n" +
                       "\n" +
                       "Pressing ^C again will terminate jobs and exit\n",
                       "Pressing ^C again will terminate jobs and exit\n",
                       err=True)
                       err=True)


            try:
            try:
                choice = click.prompt("Choice:",
                choice = click.prompt("Choice:",
                                      type=click.Choice(['continue', 'quit', 'terminate']),
                                      value_proc=prefix_choice_value_proc(['continue', 'quit', 'terminate']),
                                      default='continue', err=True)
                                      default='continue', err=True)
            except click.Abort:
            except click.Abort:
                # Ensure a newline after automatically printed '^C'
                # Ensure a newline after automatically printed '^C'
@@ -961,14 +962,14 @@ class App():
            summary = ("\n{} failure on element: {}\n".format(failure.action_name, element.name) +
            summary = ("\n{} failure on element: {}\n".format(failure.action_name, element.name) +
                       "\n" +
                       "\n" +
                       "Choose one of the following options:\n" +
                       "Choose one of the following options:\n" +
                       "  continue  - Continue queueing jobs as much as possible\n" +
                       "  (c)ontinue  - Continue queueing jobs as much as possible\n" +
                       "  quit      - Exit after all ongoing jobs complete\n" +
                       "  (q)uit      - Exit after all ongoing jobs complete\n" +
                       "  terminate - Terminate any ongoing jobs and exit\n" +
                       "  (t)erminate - Terminate any ongoing jobs and exit\n" +
                       "  retry     - Retry this job\n")
                       "  (r)etry     - Retry this job\n")
            if failure.logfile:
            if failure.logfile:
                summary += "  log       - View the full log file\n"
                summary += "  (l)og       - View the full log file\n"
            if failure.sandbox:
            if failure.sandbox:
                summary += "  shell     - Drop into a shell in the failed build sandbox\n"
                summary += "  (s)hell     - Drop into a shell in the failed build sandbox\n"
            summary += "\nPressing ^C will terminate jobs and exit\n"
            summary += "\nPressing ^C will terminate jobs and exit\n"


            choices = ['continue', 'quit', 'terminate', 'retry']
            choices = ['continue', 'quit', 'terminate', 'retry']
@@ -982,8 +983,8 @@ class App():
                click.echo(summary, err=True)
                click.echo(summary, err=True)


                try:
                try:
                    choice = click.prompt("Choice:", type=click.Choice(choices),
                    choice = click.prompt("Choice:", default='continue', err=True,
                                          default='continue', err=True)
                                          value_proc=prefix_choice_value_proc(choices))
                except click.Abort:
                except click.Abort:
                    # Ensure a newline after automatically printed '^C'
                    # Ensure a newline after automatically printed '^C'
                    click.echo("", err=True)
                    click.echo("", err=True)
@@ -1152,3 +1153,30 @@ class App():
        self.maybe_render_status()
        self.maybe_render_status()
        self.scheduler.resume_jobs()
        self.scheduler.resume_jobs()
        self.scheduler.connect_signals()
        self.scheduler.connect_signals()


#
# Return a value processor for partial choice matching.
# The returned values processor will test the passed value with all the item
# in the 'choices' list. If the value is a prefix of one of the 'choices'
# element, the element is returned. If no element or several elements match
# the same input, a 'click.UsageError' exception is raised with a description
# of the error.
#
# Note that Click expect user input errors to be signaled by raising a
# 'click.UsageError' exception. That way, Click display an error message and
# ask for a new input.
#
def prefix_choice_value_proc(choices):

    def value_proc(user_input):
        remaining_candidate = [choice for choice in choices if choice.startswith(user_input)]

        if len(remaining_candidate) == 0:
            raise UsageError("Expected one of {}, got {}".format(choices, user_input))
        elif len(remaining_candidate) == 1:
            return remaining_candidate[0]
        else:
            raise UsageError("Ambiguous input. '{}' can refer to one of {}".format(user_input, remaining_candidate))

    return value_proc

tests/frontend/main.py

0 → 100644
+34 −0
Original line number Original line Diff line number Diff line
from buildstream._frontend.main import prefix_choice_value_proc

import pytest
import click


def test_prefix_choice_value_proc_full_match():
    value_proc = prefix_choice_value_proc(['foo', 'bar', 'baz'])

    assert("foo" == value_proc("foo"))
    assert("bar" == value_proc("bar"))
    assert("baz" == value_proc("baz"))


def test_prefix_choice_value_proc_prefix_match():
    value_proc = prefix_choice_value_proc(['foo'])

    assert ("foo" == value_proc("f"))


def test_prefix_choice_value_proc_ambigous_match():
    value_proc = prefix_choice_value_proc(['bar', 'baz'])

    assert ("bar" == value_proc("bar"))
    assert ("baz" == value_proc("baz"))
    with pytest.raises(click.UsageError):
        value_proc("ba")


def test_prefix_choice_value_proc_value_not_in_choices():
    value_proc = prefix_choice_value_proc(['bar', 'baz'])

    with pytest.raises(click.UsageError):
        value_proc("foo")