summaryrefslogtreecommitdiff
path: root/tools/u_boot_pylib
diff options
context:
space:
mode:
Diffstat (limited to 'tools/u_boot_pylib')
-rwxr-xr-xtools/u_boot_pylib/__main__.py2
-rw-r--r--tools/u_boot_pylib/command.py5
-rw-r--r--tools/u_boot_pylib/gitutil.py255
-rw-r--r--tools/u_boot_pylib/terminal.py105
-rw-r--r--tools/u_boot_pylib/test_util.py29
-rw-r--r--tools/u_boot_pylib/tout.py27
6 files changed, 320 insertions, 103 deletions
diff --git a/tools/u_boot_pylib/__main__.py b/tools/u_boot_pylib/__main__.py
index c0762bca733..d86b9d7dce0 100755
--- a/tools/u_boot_pylib/__main__.py
+++ b/tools/u_boot_pylib/__main__.py
@@ -16,7 +16,7 @@ if __name__ == "__main__":
from u_boot_pylib import test_util
result = test_util.run_test_suites(
- 'u_boot_pylib', False, False, False, None, None, None,
+ 'u_boot_pylib', False, False, False, False, None, None, None,
['terminal'])
sys.exit(0 if result.wasSuccessful() else 1)
diff --git a/tools/u_boot_pylib/command.py b/tools/u_boot_pylib/command.py
index 0e247355ef6..cb7ebf49ce5 100644
--- a/tools/u_boot_pylib/command.py
+++ b/tools/u_boot_pylib/command.py
@@ -203,7 +203,7 @@ def run_one(*cmd, **kwargs):
return run_pipe([cmd], **kwargs)
-def run_list(cmd):
+def run_list(cmd, **kwargs):
"""Run a command and return its output
Args:
@@ -211,8 +211,9 @@ def run_list(cmd):
Returns:
str: output of command
+ **kwargs (dict of args): Extra arguments to pass in
"""
- return run_pipe([cmd], capture=True).stdout
+ return run_pipe([cmd], capture=True, **kwargs).stdout
def stop_all():
diff --git a/tools/u_boot_pylib/gitutil.py b/tools/u_boot_pylib/gitutil.py
index 0376bece3e6..34b4dbb4839 100644
--- a/tools/u_boot_pylib/gitutil.py
+++ b/tools/u_boot_pylib/gitutil.py
@@ -2,10 +2,11 @@
# Copyright (c) 2011 The Chromium OS Authors.
#
+"""Basic utilities for running the git command-line tool from Python"""
+
import os
import sys
-from patman import settings
from u_boot_pylib import command
from u_boot_pylib import terminal
@@ -14,7 +15,7 @@ USE_NO_DECORATE = True
def log_cmd(commit_range, git_dir=None, oneline=False, reverse=False,
- count=None):
+ count=None, decorate=False):
"""Create a command to perform a 'git log'
Args:
@@ -23,6 +24,8 @@ def log_cmd(commit_range, git_dir=None, oneline=False, reverse=False,
oneline (bool): True to use --oneline, else False
reverse (bool): True to reverse the log (--reverse)
count (int or None): Number of commits to list, or None for no limit
+ decorate (bool): True to use --decorate
+
Return:
List containing command and arguments to run
"""
@@ -32,8 +35,10 @@ def log_cmd(commit_range, git_dir=None, oneline=False, reverse=False,
cmd += ['--no-pager', 'log', '--no-color']
if oneline:
cmd.append('--oneline')
- if USE_NO_DECORATE:
+ if USE_NO_DECORATE and not decorate:
cmd.append('--no-decorate')
+ if decorate:
+ cmd.append('--decorate')
if reverse:
cmd.append('--reverse')
if count is not None:
@@ -48,7 +53,7 @@ def log_cmd(commit_range, git_dir=None, oneline=False, reverse=False,
return cmd
-def count_commits_to_branch(branch):
+def count_commits_to_branch(branch, git_dir=None, end=None):
"""Returns number of commits between HEAD and the tracking branch.
This looks back to the tracking branch and works out the number of commits
@@ -56,16 +61,22 @@ def count_commits_to_branch(branch):
Args:
branch (str or None): Branch to count from (None for current branch)
+ git_dir (str): Path to git repository (None to use default)
+ end (str): End commit to stop before
Return:
Number of patches that exist on top of the branch
"""
- if branch:
- us, _ = get_upstream('.git', branch)
+ if end:
+ rev_range = f'{end}..{branch}'
+ elif branch:
+ us, msg = get_upstream(git_dir or '.git', branch)
+ if not us:
+ raise ValueError(msg)
rev_range = f'{us}..{branch}'
else:
rev_range = '@{upstream}..'
- cmd = log_cmd(rev_range, oneline=True)
+ cmd = log_cmd(rev_range, git_dir=git_dir, oneline=True)
result = command.run_one(*cmd, capture=True, capture_stderr=True,
oneline=True, raise_on_error=False)
if result.return_code:
@@ -85,9 +96,11 @@ def name_revision(commit_hash):
Name of revision, if any, else None
"""
stdout = command.output_one_line('git', 'name-rev', commit_hash)
+ if not stdout:
+ return None
# We expect a commit, a space, then a revision name
- name = stdout.split(' ')[1].strip()
+ name = stdout.split()[1].strip()
return name
@@ -107,18 +120,21 @@ def guess_upstream(git_dir, branch):
Name of upstream branch (e.g. 'upstream/master') or None if none
Warning/error message, or None if none
"""
- cmd = log_cmd(branch, git_dir=git_dir, oneline=True, count=100)
+ cmd = log_cmd(branch, git_dir=git_dir, oneline=True, count=100,
+ decorate=True)
result = command.run_one(*cmd, capture=True, capture_stderr=True,
raise_on_error=False)
if result.return_code:
return None, f"Branch '{branch}' not found"
for line in result.stdout.splitlines()[1:]:
- commit_hash = line.split(' ')[0]
- name = name_revision(commit_hash)
- if '~' not in name and '^' not in name:
- if name.startswith('remotes/'):
- name = name[8:]
- return name, f"Guessing upstream as '{name}'"
+ parts = line.split(maxsplit=1)
+ if len(parts) >= 2 and parts[1].startswith('('):
+ commit_hash = parts[0]
+ name = name_revision(commit_hash)
+ if '~' not in name and '^' not in name:
+ if name.startswith('remotes/'):
+ name = name[8:]
+ return name, f"Guessing upstream as '{name}'"
return None, f"Cannot find a suitable upstream for branch '{branch}'"
@@ -322,7 +338,8 @@ def prune_worktrees(git_dir):
raise OSError(f'git worktree prune: {result.stderr}')
-def create_patches(branch, start, count, ignore_binary, series, signoff=True):
+def create_patches(branch, start, count, ignore_binary, series, signoff=True,
+ git_dir=None, cwd=None):
"""Create a series of patches from the top of the current branch.
The patch files are written to the current directory using
@@ -335,11 +352,16 @@ def create_patches(branch, start, count, ignore_binary, series, signoff=True):
ignore_binary (bool): Don't generate patches for binary files
series (Series): Series object for this series (set of patches)
signoff (bool): True to add signoff lines automatically
+ git_dir (str): Path to git repository (None to use default)
+ cwd (str): Path to use for git operations
Return:
Filename of cover letter (None if none)
List of filenames of patch files
"""
- cmd = ['git', 'format-patch', '-M']
+ cmd = ['git']
+ if git_dir:
+ cmd += ['--git-dir', git_dir]
+ cmd += ['format-patch', '-M']
if signoff:
cmd.append('--signoff')
if ignore_binary:
@@ -352,7 +374,7 @@ def create_patches(branch, start, count, ignore_binary, series, signoff=True):
brname = branch or 'HEAD'
cmd += [f'{brname}~{start + count}..{brname}~{start}']
- stdout = command.run_list(cmd)
+ stdout = command.run_list(cmd, cwd=cwd)
files = stdout.splitlines()
# We have an extra file if there is a cover letter
@@ -361,7 +383,7 @@ def create_patches(branch, start, count, ignore_binary, series, signoff=True):
return None, files
-def build_email_list(in_list, tag=None, alias=None, warn_on_error=True):
+def build_email_list(in_list, alias, tag=None, warn_on_error=True):
"""Build a list of email addresses based on an input list.
Takes a list of email addresses and aliases, and turns this into a list
@@ -373,10 +395,10 @@ def build_email_list(in_list, tag=None, alias=None, warn_on_error=True):
Args:
in_list (list of str): List of aliases/email addresses
- tag (str): Text to put before each address
alias (dict): Alias dictionary:
key: alias
value: list of aliases or email addresses
+ tag (str): Text to put before each address
warn_on_error (bool): True to raise an error when an alias fails to
match, False to just print a message.
@@ -389,15 +411,14 @@ def build_email_list(in_list, tag=None, alias=None, warn_on_error=True):
>>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
>>> alias['boys'] = ['fred', ' john']
>>> alias['all'] = ['fred ', 'john', ' mary ']
- >>> build_email_list(['john', 'mary'], None, alias)
+ >>> build_email_list(['john', 'mary'], alias, None)
['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
- >>> build_email_list(['john', 'mary'], '--to', alias)
+ >>> build_email_list(['john', 'mary'], alias, '--to')
['--to "j.bloggs@napier.co.nz"', \
'--to "Mary Poppins <m.poppins@cloud.net>"']
- >>> build_email_list(['john', 'mary'], 'Cc', alias)
+ >>> build_email_list(['john', 'mary'], alias, 'Cc')
['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
"""
- quote = '"' if tag and tag[0] == '-' else ''
raw = []
for item in in_list:
raw += lookup_email(item, alias, warn_on_error=warn_on_error)
@@ -406,7 +427,7 @@ def build_email_list(in_list, tag=None, alias=None, warn_on_error=True):
if item not in result:
result.append(item)
if tag:
- return [f'{tag} {quote}{email}{quote}' for email in result]
+ return [x for email in result for x in (tag, email)]
return result
@@ -437,8 +458,8 @@ def check_suppress_cc_config():
def email_patches(series, cover_fname, args, dry_run, warn_on_error, cc_fname,
- self_only=False, alias=None, in_reply_to=None, thread=False,
- smtp_server=None):
+ alias, self_only=False, in_reply_to=None, thread=False,
+ smtp_server=None, cwd=None):
"""Email a patch series.
Args:
@@ -449,15 +470,16 @@ def email_patches(series, cover_fname, args, dry_run, warn_on_error, cc_fname,
warn_on_error (bool): True to print a warning when an alias fails to
match, False to ignore it.
cc_fname (str): Filename of Cc file for per-commit Cc
- self_only (bool): True to just email to yourself as a test
- alias (dict or None): Alias dictionary: (None to use settings default)
+ alias (dict): Alias dictionary:
key: alias
value: list of aliases or email addresses
+ self_only (bool): True to just email to yourself as a test
in_reply_to (str or None): If set we'll pass this to git as
--in-reply-to - should be a message ID that this is in reply to.
thread (bool): True to add --thread to git send-email (make
all patches reply to cover-letter or first patch in series)
smtp_server (str or None): SMTP server to use to send patches
+ cwd (str): Path to use for patch files (None to use current dir)
Returns:
Git command that was/would be run
@@ -498,11 +520,10 @@ send --cc-cmd cc-fname" cover p1 p2'
# Restore argv[0] since we clobbered it.
>>> sys.argv[0] = _old_argv0
"""
- to = build_email_list(series.get('to'), '--to', alias, warn_on_error)
+ to = build_email_list(series.get('to'), alias, '--to', warn_on_error)
if not to:
- git_config_to = command.output('git', 'config', 'sendemail.to',
- raise_on_error=False)
- if not git_config_to:
+ if not command.output('git', 'config', 'sendemail.to',
+ raise_on_error=False):
print("No recipient.\n"
"Please add something like this to a commit\n"
"Series-to: Fred Bloggs <f.blogs@napier.co.nz>\n"
@@ -510,10 +531,10 @@ send --cc-cmd cc-fname" cover p1 p2'
"git config sendemail.to u-boot@lists.denx.de")
return None
cc = build_email_list(list(set(series.get('cc')) - set(series.get('to'))),
- '--cc', alias, warn_on_error)
+ alias, '--cc', warn_on_error)
if self_only:
- to = build_email_list([os.getenv('USER')], '--to',
- alias, warn_on_error)
+ to = build_email_list([os.getenv('USER')], '--to', alias,
+ warn_on_error)
cc = []
cmd = ['git', 'send-email', '--annotate']
if smtp_server:
@@ -525,24 +546,24 @@ send --cc-cmd cc-fname" cover p1 p2'
cmd += to
cmd += cc
- cmd += ['--cc-cmd', f'"{sys.argv[0]} send --cc-cmd {cc_fname}"']
+ cmd += ['--cc-cmd', f'{sys.argv[0]} send --cc-cmd {cc_fname}']
if cover_fname:
cmd.append(cover_fname)
cmd += args
- cmdstr = ' '.join(cmd)
if not dry_run:
- os.system(cmdstr)
- return cmdstr
+ command.run(*cmd, capture=False, capture_stderr=False, cwd=cwd)
+ return' '.join([f'"{x}"' if ' ' in x and '"' not in x else x
+ for x in cmd])
-def lookup_email(lookup_name, alias=None, warn_on_error=True, level=0):
+def lookup_email(lookup_name, alias, warn_on_error=True, level=0):
"""If an email address is an alias, look it up and return the full name
TODO: Why not just use git's own alias feature?
Args:
lookup_name (str): Alias or email address to look up
- alias (dict or None): Alias dictionary: (None to use settings default)
+ alias (dict): Alias dictionary
key: alias
value: list of aliases or email addresses
warn_on_error (bool): True to print a warning when an alias fails to
@@ -589,8 +610,6 @@ def lookup_email(lookup_name, alias=None, warn_on_error=True, level=0):
Recursive email alias at 'mary'
['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
"""
- if not alias:
- alias = settings.alias
lookup_name = lookup_name.strip()
if '@' in lookup_name: # Perhaps a real email address
return [lookup_name]
@@ -625,7 +644,7 @@ def get_top_level():
"""Return name of top-level directory for this git repo.
Returns:
- str: Full path to git top-level directory
+ str: Full path to git top-level directory, or None if not found
This test makes sure that we are running tests in the right subdir
@@ -633,7 +652,12 @@ def get_top_level():
os.path.join(get_top_level(), 'tools', 'patman')
True
"""
- return command.output_one_line('git', 'rev-parse', '--show-toplevel')
+ result = command.run_one(
+ 'git', 'rev-parse', '--show-toplevel', oneline=True, capture=True,
+ capture_stderr=True, raise_on_error=False)
+ if result.return_code:
+ return None
+ return result.stdout.strip()
def get_alias_file():
@@ -651,7 +675,7 @@ def get_alias_file():
if os.path.isabs(fname):
return fname
- return os.path.join(get_top_level(), fname)
+ return os.path.join(get_top_level() or '', fname)
def get_default_user_name():
@@ -693,25 +717,26 @@ def setup():
# Check for a git alias file also
global USE_NO_DECORATE
- alias_fname = get_alias_file()
- if alias_fname:
- settings.ReadGitAliases(alias_fname)
cmd = log_cmd(None, count=0)
USE_NO_DECORATE = (command.run_one(*cmd, raise_on_error=False)
.return_code == 0)
-def get_hash(spec):
+def get_hash(spec, git_dir=None):
"""Get the hash of a commit
Args:
spec (str): Git commit to show, e.g. 'my-branch~12'
+ git_dir (str): Path to git repository (None to use default)
Returns:
str: Hash of commit
"""
- return command.output_one_line('git', 'show', '-s', '--pretty=format:%H',
- spec)
+ cmd = ['git']
+ if git_dir:
+ cmd += ['--git-dir', git_dir]
+ cmd += ['show', '-s', '--pretty=format:%H', spec]
+ return command.output_one_line(*cmd)
def get_head():
@@ -723,18 +748,138 @@ def get_head():
return get_hash('HEAD')
-def get_branch():
+def get_branch(git_dir=None):
"""Get the branch we are currently on
Return:
str: branch name, or None if none
+ git_dir (str): Path to git repository (None to use default)
"""
- out = command.output_one_line('git', 'rev-parse', '--abbrev-ref', 'HEAD')
+ cmd = ['git']
+ if git_dir:
+ cmd += ['--git-dir', git_dir]
+ cmd += ['rev-parse', '--abbrev-ref', 'HEAD']
+ out = command.output_one_line(*cmd, raise_on_error=False)
if out == 'HEAD':
return None
return out
+def check_dirty(git_dir=None, work_tree=None):
+ """Check if the tree is dirty
+
+ Args:
+ git_dir (str): Path to git repository (None to use default)
+ work_tree (str): Git worktree to use, or None if none
+
+ Return:
+ str: List of dirty filenames and state
+ """
+ cmd = ['git']
+ if git_dir:
+ cmd += ['--git-dir', git_dir]
+ if work_tree:
+ cmd += ['--work-tree', work_tree]
+ cmd += ['status', '--porcelain', '--untracked-files=no']
+ return command.output(*cmd).splitlines()
+
+
+def check_branch(name, git_dir=None):
+ """Check if a branch exists
+
+ Args:
+ name (str): Name of the branch to check
+ git_dir (str): Path to git repository (None to use default)
+ """
+ cmd = ['git']
+ if git_dir:
+ cmd += ['--git-dir', git_dir]
+ cmd += ['branch', '--list', name]
+
+ # This produces ' <name>' or '* <name>'
+ out = command.output(*cmd).rstrip()
+ return out[2:] == name
+
+
+def rename_branch(old_name, name, git_dir=None):
+ """Check if a branch exists
+
+ Args:
+ old_name (str): Name of the branch to rename
+ name (str): New name for the branch
+ git_dir (str): Path to git repository (None to use default)
+
+ Return:
+ str: Output from command
+ """
+ cmd = ['git']
+ if git_dir:
+ cmd += ['--git-dir', git_dir]
+ cmd += ['branch', '--move', old_name, name]
+
+ # This produces ' <name>' or '* <name>'
+ return command.output(*cmd).rstrip()
+
+
+def get_commit_message(commit, git_dir=None):
+ """Gets the commit message for a commit
+
+ Args:
+ commit (str): commit to check
+ git_dir (str): Path to git repository (None to use default)
+
+ Return:
+ list of str: Lines from the commit message
+ """
+ cmd = ['git']
+ if git_dir:
+ cmd += ['--git-dir', git_dir]
+ cmd += ['show', '--quiet', commit]
+
+ out = command.output(*cmd)
+ # the header is followed by a blank line
+ lines = out.splitlines()
+ empty = lines.index('')
+ msg = lines[empty + 1:]
+ unindented = [line[4:] for line in msg]
+
+ return unindented
+
+
+def show_commit(commit, msg=True, diffstat=False, patch=False, colour=True,
+ git_dir=None):
+ """Runs 'git show' and returns the output
+
+ Args:
+ commit (str): commit to check
+ msg (bool): Show the commit message
+ diffstat (bool): True to include the diffstat
+ patch (bool): True to include the patch
+ colour (bool): True to force use of colour
+ git_dir (str): Path to git repository (None to use default)
+
+ Return:
+ list of str: Lines from the commit message
+ """
+ cmd = ['git']
+ if git_dir:
+ cmd += ['--git-dir', git_dir]
+ cmd += ['show']
+ if colour:
+ cmd.append('--color')
+ if not msg:
+ cmd.append('--oneline')
+ if diffstat:
+ cmd.append('--stat')
+ else:
+ cmd.append('--quiet')
+ if patch:
+ cmd.append('--patch')
+ cmd.append(commit)
+
+ return command.output(*cmd)
+
+
if __name__ == "__main__":
import doctest
diff --git a/tools/u_boot_pylib/terminal.py b/tools/u_boot_pylib/terminal.py
index 2cd5a54ab52..69c183e85e5 100644
--- a/tools/u_boot_pylib/terminal.py
+++ b/tools/u_boot_pylib/terminal.py
@@ -7,9 +7,12 @@
This module handles terminal interaction including ANSI color codes.
"""
+from contextlib import contextmanager
+from io import StringIO
import os
import re
import shutil
+import subprocess
import sys
# Selection of when we want our output to be colored
@@ -26,6 +29,13 @@ last_print_len = None
# stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python
ansi_escape = re.compile(r'\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
+# True if we are capturing console output
+CAPTURING = False
+
+# Set this to False to disable output-capturing globally
+USE_CAPTURE = True
+
+
class PrintLine:
"""A line of text output
@@ -130,7 +140,8 @@ def trim_ascii_len(text, size):
return out
-def tprint(text='', newline=True, colour=None, limit_to_line=False, bright=True):
+def tprint(text='', newline=True, colour=None, limit_to_line=False,
+ bright=True, back=None, col=None):
"""Handle a line of output to the terminal.
In test mode this is recorded in a list. Otherwise it is output to the
@@ -146,9 +157,10 @@ def tprint(text='', newline=True, colour=None, limit_to_line=False, bright=True)
if print_test_mode:
print_test_list.append(PrintLine(text, colour, newline, bright))
else:
- if colour:
- col = Color()
- text = col.build(colour, text, bright=bright)
+ if colour is not None:
+ if not col:
+ col = Color()
+ text = col.build(colour, text, bright=bright, back=back)
if newline:
print(text)
last_print_len = None
@@ -200,14 +212,23 @@ def echo_print_test_lines():
if line.newline:
print()
+def have_terminal():
+ """Check if we have an interactive terminal or not
+
+ Returns:
+ bool: true if an interactive terminal is attached
+ """
+ return os.isatty(sys.stdout.fileno())
+
-class Color(object):
+class Color():
"""Conditionally wraps text in ANSI color escape sequences."""
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
BOLD = -1
- BRIGHT_START = '\033[1;%dm'
- NORMAL_START = '\033[22;%dm'
+ BRIGHT_START = '\033[1;%d%sm'
+ NORMAL_START = '\033[22;%d%sm'
BOLD_START = '\033[1m'
+ BACK_EXTRA = ';%d'
RESET = '\033[0m'
def __init__(self, colored=COLOR_IF_TERMINAL):
@@ -224,7 +245,14 @@ class Color(object):
except:
self._enabled = False
- def start(self, color, bright=True):
+ def enabled(self):
+ """Check if colour is enabled
+
+ Return: True if enabled, else False
+ """
+ return self._enabled
+
+ def start(self, color, bright=True, back=None):
"""Returns a start color code.
Args:
@@ -235,8 +263,11 @@ class Color(object):
color, otherwise returns empty string
"""
if self._enabled:
+ if color == self.BOLD:
+ return self.BOLD_START
base = self.BRIGHT_START if bright else self.NORMAL_START
- return base % (color + 30)
+ extra = self.BACK_EXTRA % (back + 40) if back else ''
+ return base % (color + 30, extra)
return ''
def stop(self):
@@ -250,7 +281,7 @@ class Color(object):
return self.RESET
return ''
- def build(self, color, text, bright=True):
+ def build(self, color, text, bright=True, back=None):
"""Returns text with conditionally added color escape sequences.
Keyword arguments:
@@ -265,9 +296,51 @@ class Color(object):
"""
if not self._enabled:
return text
- if color == self.BOLD:
- start = self.BOLD_START
- else:
- base = self.BRIGHT_START if bright else self.NORMAL_START
- start = base % (color + 30)
- return start + text + self.RESET
+ return self.start(color, bright, back) + text + self.RESET
+
+
+# Use this to suppress stdout/stderr output:
+# with terminal.capture() as (stdout, stderr)
+# ...do something...
+@contextmanager
+def capture():
+ global CAPTURING
+
+ capture_out, capture_err = StringIO(), StringIO()
+ old_out, old_err = sys.stdout, sys.stderr
+ try:
+ CAPTURING = True
+ sys.stdout, sys.stderr = capture_out, capture_err
+ yield capture_out, capture_err
+ finally:
+ sys.stdout, sys.stderr = old_out, old_err
+ CAPTURING = False
+ if not USE_CAPTURE:
+ sys.stdout.write(capture_out.getvalue())
+ sys.stderr.write(capture_err.getvalue())
+
+
+@contextmanager
+def pager():
+ """Simple pager for outputting lots of text
+
+ Usage:
+ with terminal.pager():
+ print(...)
+ """
+ proc = None
+ old_stdout = None
+ try:
+ less = os.getenv('PAGER')
+ if not CAPTURING and less != 'none' and have_terminal():
+ if not less:
+ less = 'less -R --quit-if-one-screen'
+ proc = subprocess.Popen(less, stdin=subprocess.PIPE, text=True,
+ shell=True)
+ old_stdout = sys.stdout
+ sys.stdout = proc.stdin
+ yield
+ finally:
+ if proc:
+ sys.stdout = old_stdout
+ proc.communicate()
diff --git a/tools/u_boot_pylib/test_util.py b/tools/u_boot_pylib/test_util.py
index 637403f8715..d258a1935c9 100644
--- a/tools/u_boot_pylib/test_util.py
+++ b/tools/u_boot_pylib/test_util.py
@@ -3,7 +3,6 @@
# Copyright (c) 2016 Google, Inc
#
-from contextlib import contextmanager
import doctest
import glob
import multiprocessing
@@ -13,8 +12,7 @@ import sys
import unittest
from u_boot_pylib import command
-
-from io import StringIO
+from u_boot_pylib import terminal
use_concurrent = True
try:
@@ -113,20 +111,6 @@ def run_test_coverage(prog, filter_fname, exclude_list, build_dir,
raise ValueError('Test coverage failure')
-# Use this to suppress stdout/stderr output:
-# with capture_sys_output() as (stdout, stderr)
-# ...do something...
-@contextmanager
-def capture_sys_output():
- capture_out, capture_err = StringIO(), StringIO()
- old_out, old_err = sys.stdout, sys.stderr
- try:
- sys.stdout, sys.stderr = capture_out, capture_err
- yield capture_out, capture_err
- finally:
- sys.stdout, sys.stderr = old_out, old_err
-
-
class FullTextTestResult(unittest.TextTestResult):
"""A test result class that can print extended text results to a stream
@@ -172,8 +156,8 @@ class FullTextTestResult(unittest.TextTestResult):
super().addSkip(test, reason)
-def run_test_suites(toolname, debug, verbosity, test_preserve_dirs, processes,
- test_name, toolpath, class_and_module_list):
+def run_test_suites(toolname, debug, verbosity, no_capture, test_preserve_dirs,
+ processes, test_name, toolpath, class_and_module_list):
"""Run a series of test suites and collect the results
Args:
@@ -196,6 +180,9 @@ def run_test_suites(toolname, debug, verbosity, test_preserve_dirs, processes,
sys.argv.append('-D')
if verbosity:
sys.argv.append('-v%d' % verbosity)
+ if no_capture:
+ sys.argv.append('-N')
+ terminal.USE_CAPTURE = False
if toolpath:
for path in toolpath:
sys.argv += ['--toolpath', path]
@@ -208,7 +195,7 @@ def run_test_suites(toolname, debug, verbosity, test_preserve_dirs, processes,
resultclass=FullTextTestResult,
)
- if use_concurrent and processes != 1:
+ if use_concurrent and processes != 1 and not test_name:
suite = ConcurrentTestSuite(suite,
fork_for_tests(processes or multiprocessing.cpu_count()))
@@ -224,7 +211,7 @@ def run_test_suites(toolname, debug, verbosity, test_preserve_dirs, processes,
setup_test_args = getattr(module, 'setup_test_args')
setup_test_args(preserve_indir=test_preserve_dirs,
preserve_outdirs=test_preserve_dirs and test_name is not None,
- toolpath=toolpath, verbosity=verbosity)
+ toolpath=toolpath, verbosity=verbosity, no_capture=no_capture)
if test_name:
# Since Python v3.5 If an ImportError or AttributeError occurs
# while traversing a name then a synthetic test that raises that
diff --git a/tools/u_boot_pylib/tout.py b/tools/u_boot_pylib/tout.py
index 6bd2806f88f..ca72108d6bc 100644
--- a/tools/u_boot_pylib/tout.py
+++ b/tools/u_boot_pylib/tout.py
@@ -9,7 +9,7 @@ import sys
from u_boot_pylib import terminal
# Output verbosity levels that we support
-ERROR, WARNING, NOTICE, INFO, DETAIL, DEBUG = range(6)
+FATAL, ERROR, WARNING, NOTICE, INFO, DETAIL, DEBUG = range(7)
in_progress = False
@@ -42,12 +42,12 @@ def user_is_present():
Returns:
True if it thinks the user is there, and False otherwise
"""
- return stdout_is_tty and verbose > 0
+ return stdout_is_tty and verbose > ERROR
def clear_progress():
"""Clear any active progress message on the terminal."""
global in_progress
- if verbose > 0 and stdout_is_tty and in_progress:
+ if verbose > ERROR and stdout_is_tty and in_progress:
_stdout.write('\r%s\r' % (" " * len (_progress)))
_stdout.flush()
in_progress = False
@@ -60,7 +60,7 @@ def progress(msg, warning=False, trailer='...'):
warning: True if this is a warning."""
global in_progress
clear_progress()
- if verbose > 0:
+ if verbose > ERROR:
_progress = msg + trailer
if stdout_is_tty:
col = _color.YELLOW if warning else _color.GREEN
@@ -87,6 +87,8 @@ def _output(level, msg, color=None):
print(msg, file=sys.stderr)
else:
print(msg)
+ if level == FATAL:
+ sys.exit(1)
def do_output(level, msg):
"""Output a message to the terminal.
@@ -98,6 +100,14 @@ def do_output(level, msg):
"""
_output(level, msg)
+def fatal(msg):
+ """Display an error message and exit
+
+ Args:
+ msg; Message to display.
+ """
+ _output(FATAL, msg, _color.RED)
+
def error(msg):
"""Display an error message
@@ -153,20 +163,21 @@ def user_output(msg):
Args:
msg; Message to display.
"""
- _output(0, msg)
+ _output(ERROR, msg)
-def init(_verbose=WARNING, stdout=sys.stdout):
+def init(_verbose=WARNING, stdout=sys.stdout, allow_colour=True):
"""Initialize a new output object.
Args:
- verbose: Verbosity level (0-4).
+ verbose: Verbosity level (0-6).
stdout: File to use for stdout.
"""
global verbose, _progress, _color, _stdout, stdout_is_tty
verbose = _verbose
_progress = '' # Our last progress message
- _color = terminal.Color()
+ _color = terminal.Color(terminal.COLOR_IF_TERMINAL if allow_colour
+ else terminal.COLOR_NEVER)
_stdout = stdout
# TODO(sjg): Move this into Chromite libraries when we have them