Applies the `pyupgrade` hook to all files in the repo. Change-Id: I9879c634a65c5fcaa9567c63bc5977ff97d5d3bf
297 lines
9.4 KiB
Python
Executable File
297 lines
9.4 KiB
Python
Executable File
#! /usr/bin/env python3
|
|
#
|
|
# Copyright 2021 Google Inc.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions are
|
|
# met: redistributions of source code must retain the above copyright
|
|
# notice, this list of conditions and the following disclaimer;
|
|
# redistributions in binary form must reproduce the above copyright
|
|
# notice, this list of conditions and the following disclaimer in the
|
|
# documentation and/or other materials provided with the distribution;
|
|
# neither the name of the copyright holders nor the names of its
|
|
# contributors may be used to endorse or promote products derived from
|
|
# this software without specific prior written permission.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
import argparse
|
|
import copy
|
|
import signal
|
|
import sys
|
|
import unittest
|
|
import unittest.mock
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description="""Circular buffer for text output.
|
|
|
|
To capture the rolling last 25 lines of output from command "command":
|
|
command | logroll.py -n 25
|
|
|
|
While that's running, to see the most recent 25 lines of output without
|
|
interrupting "command", send SIGUSR1 to the logroll.py process:
|
|
kill -s USR1 ${PID of logroll.py}"""
|
|
)
|
|
parser.add_argument(
|
|
"-n",
|
|
"--lines",
|
|
default=10,
|
|
type=int,
|
|
help="Maximum number of lines to buffer at a time.",
|
|
)
|
|
parser.add_argument(
|
|
"file",
|
|
nargs="?",
|
|
default=sys.stdin,
|
|
type=argparse.FileType("r", encoding="UTF-8"),
|
|
help="File to read from, default is stdin",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
def dump_lines(lines, idx):
|
|
for line in lines[idx:]:
|
|
print(line, end="")
|
|
for line in lines[:idx]:
|
|
print(line, end="")
|
|
|
|
|
|
def dump_and_exit(lines, idx):
|
|
dump_lines(lines, idx)
|
|
sys.exit(0)
|
|
|
|
|
|
def main(target, incoming):
|
|
idx = 0
|
|
lines = []
|
|
last_idx = target - 1
|
|
|
|
signal.signal(signal.SIGUSR1, lambda num, frame: dump_lines(lines, idx))
|
|
signal.signal(signal.SIGINT, lambda num, frame: dump_and_exit(lines, idx))
|
|
|
|
for line in incoming:
|
|
lines.append(line)
|
|
if idx == last_idx:
|
|
idx = 0
|
|
break
|
|
else:
|
|
idx += 1
|
|
|
|
for lines[idx] in incoming:
|
|
idx = 0 if idx == last_idx else idx + 1
|
|
|
|
dump_lines(lines, idx)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main(target=args.lines, incoming=args.file)
|
|
|
|
|
|
# Unit tests #
|
|
|
|
|
|
class CopyingMock(unittest.mock.MagicMock):
|
|
def __call__(self, *args, **kwargs):
|
|
args = copy.deepcopy(args)
|
|
kwargs = copy.deepcopy(kwargs)
|
|
return super().__call__(*args, **kwargs)
|
|
|
|
|
|
class TestLogroll(unittest.TestCase):
|
|
# Test data.
|
|
lines2 = ["First line", "Second line"]
|
|
lines3 = ["First line", "Second line", "Third line"]
|
|
lines8 = [
|
|
"First line",
|
|
"Second line",
|
|
"Third line",
|
|
"Fourth line",
|
|
"Fifth line",
|
|
"Sixth line",
|
|
"Seventh line",
|
|
"Eigth line",
|
|
]
|
|
|
|
# Generator which returns lines like a file object would.
|
|
def line_gen(self, lines):
|
|
yield from lines
|
|
|
|
# Generator like above, but which simulates a signal midway through.
|
|
def signal_line_gen(self, lines, pos, sig_dict, signal):
|
|
# Return the first few lines.
|
|
yield from lines[:pos]
|
|
|
|
# Simulate receiving the signal.
|
|
self.assertIn(signal, sig_dict)
|
|
if signal in sig_dict:
|
|
# Pas in junk for the num and frame arguments.
|
|
sig_dict[signal](None, None)
|
|
|
|
# Return the remaining lines.
|
|
yield from lines[pos:]
|
|
|
|
# Set up a mock of signal.signal to record handlers in a dict.
|
|
def mock_signal_dict(self, mock):
|
|
signal_dict = {}
|
|
|
|
def signal_signal(num, action):
|
|
signal_dict[num] = action
|
|
|
|
mock.side_effect = signal_signal
|
|
return signal_dict
|
|
|
|
# Actual test methods.
|
|
def test_filling_dump_lines(self):
|
|
with unittest.mock.patch("builtins.print") as mock_print:
|
|
dump_lines(self.lines2, len(self.lines2))
|
|
calls = list(
|
|
[unittest.mock.call(line, end="") for line in self.lines2]
|
|
)
|
|
mock_print.assert_has_calls(calls)
|
|
|
|
def test_full_dump_lines(self):
|
|
with unittest.mock.patch("builtins.print") as mock_print:
|
|
dump_lines(self.lines2, 0)
|
|
calls = list(
|
|
[unittest.mock.call(line, end="") for line in self.lines2]
|
|
)
|
|
mock_print.assert_has_calls(calls)
|
|
|
|
def test_offset_dump_lines(self):
|
|
with unittest.mock.patch("builtins.print") as mock_print:
|
|
dump_lines(self.lines3, 1)
|
|
calls = [
|
|
unittest.mock.call(self.lines3[1], end=""),
|
|
unittest.mock.call(self.lines3[2], end=""),
|
|
unittest.mock.call(self.lines3[0], end=""),
|
|
]
|
|
mock_print.assert_has_calls(calls)
|
|
|
|
def test_dump_and_exit(self):
|
|
with unittest.mock.patch(
|
|
"sys.exit"
|
|
) as mock_sys_exit, unittest.mock.patch(
|
|
__name__ + ".dump_lines", new_callable=CopyingMock
|
|
) as mock_dump_lines:
|
|
idx = 1
|
|
dump_and_exit(self.lines3, idx)
|
|
mock_dump_lines.assert_called_with(self.lines3, idx)
|
|
mock_sys_exit.assert_called_with(0)
|
|
|
|
def test_filling_main(self):
|
|
with unittest.mock.patch("builtins.print") as mock_print:
|
|
main(5, self.line_gen(self.lines3))
|
|
calls = list(
|
|
[unittest.mock.call(line, end="") for line in self.lines3]
|
|
)
|
|
mock_print.assert_has_calls(calls)
|
|
|
|
def test_full_main(self):
|
|
with unittest.mock.patch("builtins.print") as mock_print:
|
|
main(5, self.line_gen(self.lines8))
|
|
calls = list(
|
|
[unittest.mock.call(line, end="") for line in self.lines8[-5:]]
|
|
)
|
|
mock_print.assert_has_calls(calls)
|
|
|
|
def test_sigusr1_filling_main(self):
|
|
with unittest.mock.patch(
|
|
"signal.signal"
|
|
) as mock_signal, unittest.mock.patch(
|
|
__name__ + ".dump_lines", new_callable=CopyingMock
|
|
) as mock_dump_lines:
|
|
signal_dict = self.mock_signal_dict(mock_signal)
|
|
|
|
main(
|
|
4,
|
|
self.signal_line_gen(
|
|
self.lines8, 3, signal_dict, signal.SIGUSR1
|
|
),
|
|
)
|
|
|
|
mock_dump_lines.assert_has_calls(
|
|
[
|
|
unittest.mock.call(self.lines8[0:3], 3 % 4),
|
|
unittest.mock.call(self.lines8[-4:], len(self.lines8) % 4),
|
|
]
|
|
)
|
|
|
|
def test_sigint_filling_main(self):
|
|
with unittest.mock.patch(
|
|
"signal.signal"
|
|
) as mock_signal, unittest.mock.patch(
|
|
__name__ + ".dump_lines", new_callable=CopyingMock
|
|
) as mock_dump_lines:
|
|
signal_dict = self.mock_signal_dict(mock_signal)
|
|
|
|
with self.assertRaises(SystemExit):
|
|
main(
|
|
4,
|
|
self.signal_line_gen(
|
|
self.lines8, 3, signal_dict, signal.SIGINT
|
|
),
|
|
)
|
|
|
|
mock_dump_lines.assert_has_calls(
|
|
[unittest.mock.call(self.lines8[0:3], 3 % 4)]
|
|
)
|
|
|
|
def test_sigusr1_full_main(self):
|
|
with unittest.mock.patch(
|
|
"signal.signal"
|
|
) as mock_signal, unittest.mock.patch(
|
|
__name__ + ".dump_lines", new_callable=CopyingMock
|
|
) as mock_dump_lines:
|
|
signal_dict = self.mock_signal_dict(mock_signal)
|
|
|
|
main(
|
|
4,
|
|
self.signal_line_gen(
|
|
self.lines8, 5, signal_dict, signal.SIGUSR1
|
|
),
|
|
)
|
|
|
|
mock_dump_lines.assert_has_calls(
|
|
[
|
|
unittest.mock.call(
|
|
self.lines8[4:5] + self.lines8[1:4], 5 % 4
|
|
),
|
|
unittest.mock.call(self.lines8[-4:], len(self.lines8) % 4),
|
|
]
|
|
)
|
|
|
|
def test_sigint_full_main(self):
|
|
with unittest.mock.patch(
|
|
"signal.signal"
|
|
) as mock_signal, unittest.mock.patch(
|
|
__name__ + ".dump_lines", new_callable=CopyingMock
|
|
) as mock_dump_lines:
|
|
signal_dict = self.mock_signal_dict(mock_signal)
|
|
|
|
with self.assertRaises(SystemExit):
|
|
main(
|
|
4,
|
|
self.signal_line_gen(
|
|
self.lines8, 5, signal_dict, signal.SIGINT
|
|
),
|
|
)
|
|
|
|
mock_dump_lines.assert_has_calls(
|
|
[
|
|
unittest.mock.call(
|
|
self.lines8[4:5] + self.lines8[1:4], 5 % 4
|
|
)
|
|
]
|
|
)
|