pyallel 2.0.6__tar.gz → 2.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyallel-2.1.0/.vim/project.shada +0 -0
- {pyallel-2.0.6 → pyallel-2.1.0}/PKG-INFO +4 -3
- {pyallel-2.0.6 → pyallel-2.1.0}/README.md +3 -2
- {pyallel-2.0.6 → pyallel-2.1.0}/pyproject.toml +1 -1
- pyallel-2.1.0/src/pyallel/logging.py +14 -0
- {pyallel-2.0.6 → pyallel-2.1.0}/src/pyallel/main.py +9 -9
- {pyallel-2.0.6 → pyallel-2.1.0}/src/pyallel/parser.py +12 -3
- {pyallel-2.0.6 → pyallel-2.1.0}/src/pyallel/printer.py +42 -3
- {pyallel-2.0.6 → pyallel-2.1.0}/src/pyallel/process.py +0 -14
- {pyallel-2.0.6 → pyallel-2.1.0}/src/pyallel/process_group_manager.py +2 -2
- pyallel-2.1.0/tests/conftest.py +13 -0
- {pyallel-2.0.6 → pyallel-2.1.0}/tests/test_main.py +0 -88
- {pyallel-2.0.6 → pyallel-2.1.0}/tests/test_process.py +5 -36
- {pyallel-2.0.6 → pyallel-2.1.0}/tests/test_process_group.py +5 -3
- {pyallel-2.0.6 → pyallel-2.1.0}/tests/test_process_manager.py +61 -15
- {pyallel-2.0.6 → pyallel-2.1.0}/uv.lock +1 -1
- pyallel-2.0.6/.vim/project.shada +0 -0
- {pyallel-2.0.6 → pyallel-2.1.0}/.base_branch +0 -0
- {pyallel-2.0.6 → pyallel-2.1.0}/.github/workflows/build.yml +0 -0
- {pyallel-2.0.6 → pyallel-2.1.0}/.github/workflows/test.yml +0 -0
- {pyallel-2.0.6 → pyallel-2.1.0}/.gitignore +0 -0
- {pyallel-2.0.6 → pyallel-2.1.0}/Dockerfile +0 -0
- {pyallel-2.0.6 → pyallel-2.1.0}/Dockerfile.alpine +0 -0
- {pyallel-2.0.6 → pyallel-2.1.0}/LICENSE +0 -0
- {pyallel-2.0.6 → pyallel-2.1.0}/benchmark_process.py +0 -0
- {pyallel-2.0.6 → pyallel-2.1.0}/bin/ldd +0 -0
- {pyallel-2.0.6 → pyallel-2.1.0}/bin/pyinstaller.sh +0 -0
- {pyallel-2.0.6 → pyallel-2.1.0}/build.sh +0 -0
- {pyallel-2.0.6 → pyallel-2.1.0}/build_all.sh +0 -0
- {pyallel-2.0.6 → pyallel-2.1.0}/src/pyallel/__init__.py +0 -0
- {pyallel-2.0.6 → pyallel-2.1.0}/src/pyallel/colours.py +0 -0
- {pyallel-2.0.6 → pyallel-2.1.0}/src/pyallel/constants.py +0 -0
- {pyallel-2.0.6 → pyallel-2.1.0}/src/pyallel/errors.py +0 -0
- {pyallel-2.0.6 → pyallel-2.1.0}/src/pyallel/process_group.py +0 -0
- {pyallel-2.0.6 → pyallel-2.1.0}/src/pyallel/py.typed +0 -0
- {pyallel-2.0.6 → pyallel-2.1.0}/tests/assets/test_handle_multiple_signals.sh +0 -0
- {pyallel-2.0.6 → pyallel-2.1.0}/tests/assets/test_output.sh +0 -0
- {pyallel-2.0.6 → pyallel-2.1.0}/tests/test_printer.py +0 -0
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyallel
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Run and handle the output of multiple executables in pyallel (as in parallel)
|
|
5
5
|
Project-URL: Homepage, https://github.com/Danthewaann/pyallel
|
|
6
6
|
Project-URL: Repository, https://github.com/Danthewaann/pyallel
|
|
@@ -39,7 +39,7 @@ pipx install pyallel
|
|
|
39
39
|
Once installed, you can run `pyallel` to see usage information, like so:
|
|
40
40
|
|
|
41
41
|
```
|
|
42
|
-
usage: pyallel [-h] [-t] [-n] [-V] [--colour {yes,no,auto}] [commands ...]
|
|
42
|
+
usage: pyallel [-h] [-t] [-n] [-V] [--colour {yes,no,auto}] [--debug] [commands ...]
|
|
43
43
|
|
|
44
44
|
run and handle the output of multiple executables in pyallel (as in parallel)
|
|
45
45
|
|
|
@@ -98,6 +98,7 @@ options:
|
|
|
98
98
|
-V, --version print version and exit
|
|
99
99
|
--colour {yes,no,auto}
|
|
100
100
|
colour terminal output, defaults to "auto"
|
|
101
|
+
--debug enable debug mode, which logs debug info to a "pyallel.log" file in the current directory
|
|
101
102
|
```
|
|
102
103
|
|
|
103
104
|
Currently you can provide a variable number of `commands` to run to `pyallel`, like so:
|
|
@@ -158,7 +159,7 @@ pipx install uv && \
|
|
|
158
159
|
before a given command can start)
|
|
159
160
|
- [x] Add support to state how many lines a command can use for it's output in interactive mode
|
|
160
161
|
- [x] Improve printing of output performance by only printing lines that have changed
|
|
161
|
-
- [
|
|
162
|
+
- [x] Add a debug mode that logs debug information to a log file
|
|
162
163
|
- [ ] Maybe add support to allow the user to provide stdin for commands that request it
|
|
163
164
|
(such as a REPL)
|
|
164
165
|
- [ ] Add custom parsing of command output to support filtering for errors (like vim's
|
|
@@ -21,7 +21,7 @@ pipx install pyallel
|
|
|
21
21
|
Once installed, you can run `pyallel` to see usage information, like so:
|
|
22
22
|
|
|
23
23
|
```
|
|
24
|
-
usage: pyallel [-h] [-t] [-n] [-V] [--colour {yes,no,auto}] [commands ...]
|
|
24
|
+
usage: pyallel [-h] [-t] [-n] [-V] [--colour {yes,no,auto}] [--debug] [commands ...]
|
|
25
25
|
|
|
26
26
|
run and handle the output of multiple executables in pyallel (as in parallel)
|
|
27
27
|
|
|
@@ -80,6 +80,7 @@ options:
|
|
|
80
80
|
-V, --version print version and exit
|
|
81
81
|
--colour {yes,no,auto}
|
|
82
82
|
colour terminal output, defaults to "auto"
|
|
83
|
+
--debug enable debug mode, which logs debug info to a "pyallel.log" file in the current directory
|
|
83
84
|
```
|
|
84
85
|
|
|
85
86
|
Currently you can provide a variable number of `commands` to run to `pyallel`, like so:
|
|
@@ -140,7 +141,7 @@ pipx install uv && \
|
|
|
140
141
|
before a given command can start)
|
|
141
142
|
- [x] Add support to state how many lines a command can use for it's output in interactive mode
|
|
142
143
|
- [x] Improve printing of output performance by only printing lines that have changed
|
|
143
|
-
- [
|
|
144
|
+
- [x] Add a debug mode that logs debug information to a log file
|
|
144
145
|
- [ ] Maybe add support to allow the user to provide stdin for commands that request it
|
|
145
146
|
(such as a REPL)
|
|
146
147
|
- [ ] Add custom parsing of command output to support filtering for errors (like vim's
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def configure_logging(*, debug: bool = False) -> None:
|
|
6
|
+
if debug:
|
|
7
|
+
logging.basicConfig(
|
|
8
|
+
filename="pyallel.log",
|
|
9
|
+
format="%(asctime)s:%(name)s:%(lineno)d:%(message)s",
|
|
10
|
+
datefmt="%Y-%m-%dT%H:%M:%S",
|
|
11
|
+
level=logging.DEBUG,
|
|
12
|
+
)
|
|
13
|
+
else:
|
|
14
|
+
logging.basicConfig(handlers=[logging.StreamHandler(stream=open(os.devnull, "w"))]) # noqa: PTH123, SIM115
|
|
@@ -9,6 +9,7 @@ import traceback
|
|
|
9
9
|
from pyallel import constants
|
|
10
10
|
from pyallel.colours import Colours
|
|
11
11
|
from pyallel.errors import PyallelError
|
|
12
|
+
from pyallel.logging import configure_logging
|
|
12
13
|
from pyallel.parser import Arguments, create_parser
|
|
13
14
|
from pyallel.printer import (
|
|
14
15
|
InteractiveConsolePrinter,
|
|
@@ -17,13 +18,6 @@ from pyallel.printer import (
|
|
|
17
18
|
)
|
|
18
19
|
from pyallel.process_group_manager import ProcessGroupManager
|
|
19
20
|
|
|
20
|
-
logging.basicConfig(
|
|
21
|
-
filename="pyallel.log",
|
|
22
|
-
format="%(asctime)s:%(levelname)s:%(name)s:%(lineno)d[%(threadName)s]:%(message)s",
|
|
23
|
-
datefmt="%Y-%m-%dT%H:%M:%S",
|
|
24
|
-
level=logging.DEBUG,
|
|
25
|
-
)
|
|
26
|
-
|
|
27
21
|
logger = logging.getLogger(__name__)
|
|
28
22
|
|
|
29
23
|
|
|
@@ -41,6 +35,8 @@ def entry_point(*args: str) -> int:
|
|
|
41
35
|
parser.print_help()
|
|
42
36
|
return 2
|
|
43
37
|
|
|
38
|
+
configure_logging(debug=parsed_args.debug)
|
|
39
|
+
|
|
44
40
|
colours = Colours.from_colour(parsed_args.colour)
|
|
45
41
|
printer: Printer
|
|
46
42
|
if not parsed_args.interactive or not constants.IN_TTY:
|
|
@@ -54,19 +50,24 @@ def entry_point(*args: str) -> int:
|
|
|
54
50
|
print(f"{colours.red_bold}Error: {e!s}{colours.reset_colour}")
|
|
55
51
|
return 1
|
|
56
52
|
|
|
53
|
+
logger.debug("starting run with arguments:\n%s", parsed_args)
|
|
57
54
|
try:
|
|
58
55
|
exit_code = run(process_group_manager, printer)
|
|
59
56
|
except Exception:
|
|
57
|
+
logger.exception("failed run with arguments:\n%s", parsed_args)
|
|
60
58
|
print(f"{colours.red_bold}Error: {traceback.format_exc()}{colours.reset_colour}")
|
|
61
59
|
return 1
|
|
62
60
|
|
|
63
61
|
if exit_code == 1:
|
|
62
|
+
logger.error("failed run with arguments:\n%s", parsed_args)
|
|
64
63
|
process_group = process_group_manager.get_cur_process_group_output()
|
|
65
64
|
print(f"\n{colours.red_bold}ERROR: the following commands failed{colours.reset_colour}")
|
|
66
65
|
for process_output in process_group.processes:
|
|
67
66
|
process_poll = process_output.process.poll()
|
|
68
67
|
if process_poll and process_poll > 0:
|
|
69
68
|
print(f" {colours.red_bold}{process_output.process.command}{colours.reset_colour}")
|
|
69
|
+
else:
|
|
70
|
+
logger.debug("finished run with arguments:\n%s", parsed_args)
|
|
70
71
|
|
|
71
72
|
return exit_code
|
|
72
73
|
|
|
@@ -81,8 +82,7 @@ def run(process_group_manager: ProcessGroupManager, printer: Printer) -> int:
|
|
|
81
82
|
if poll is not None:
|
|
82
83
|
# If we still have new output to print after the process group manager has completed,
|
|
83
84
|
# make sure to print it here before continuing
|
|
84
|
-
|
|
85
|
-
if has_output:
|
|
85
|
+
if process_group_manager.stream().has_output():
|
|
86
86
|
printer.print(process_group_manager)
|
|
87
87
|
|
|
88
88
|
if poll > 0:
|
|
@@ -10,13 +10,16 @@ class Arguments:
|
|
|
10
10
|
interactive: bool
|
|
11
11
|
timer: bool
|
|
12
12
|
version: bool
|
|
13
|
+
debug: bool
|
|
13
14
|
|
|
14
15
|
def __repr__(self) -> str:
|
|
15
|
-
msg = "
|
|
16
|
+
msg = ""
|
|
16
17
|
padding = len(sorted(self.__dict__.keys(), key=len, reverse=True)[0]) + 1
|
|
17
18
|
for field, value in self.__dict__.items():
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
if field == "commands":
|
|
20
|
+
value = " ".join(value) # noqa: PLW2901
|
|
21
|
+
msg += f"{field: <{padding}}: {value}\n"
|
|
22
|
+
return msg.strip()
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
DESCRIPTION = r"""run and handle the output of multiple executables in %(prog)s (as in parallel)
|
|
@@ -108,5 +111,11 @@ def create_parser() -> ArgumentParser:
|
|
|
108
111
|
choices=("yes", "no", "auto"),
|
|
109
112
|
default="auto",
|
|
110
113
|
)
|
|
114
|
+
parser.add_argument(
|
|
115
|
+
"--debug",
|
|
116
|
+
help='enable debug mode, which logs debug info to a "pyallel.log" file in the current directory',
|
|
117
|
+
action="store_true",
|
|
118
|
+
default=False,
|
|
119
|
+
)
|
|
111
120
|
|
|
112
121
|
return parser
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
import time
|
|
4
5
|
from typing import TYPE_CHECKING, Protocol
|
|
5
6
|
|
|
@@ -12,6 +13,9 @@ if TYPE_CHECKING:
|
|
|
12
13
|
from pyallel.process_group_manager import ProcessGroupManager
|
|
13
14
|
|
|
14
15
|
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
15
19
|
class Printer(Protocol):
|
|
16
20
|
def print(self, process_group_manager: ProcessGroupManager) -> None:
|
|
17
21
|
"""Print output obtained from the provided process group manager.
|
|
@@ -195,7 +199,7 @@ class ConsolePrinter:
|
|
|
195
199
|
|
|
196
200
|
return self._to_print
|
|
197
201
|
|
|
198
|
-
def set_process_lines(
|
|
202
|
+
def set_process_lines( # noqa: PLR0915
|
|
199
203
|
self,
|
|
200
204
|
output: ProcessGroupOutput,
|
|
201
205
|
interrupt_count: int = 0,
|
|
@@ -205,8 +209,10 @@ class ConsolePrinter:
|
|
|
205
209
|
if interrupt_count:
|
|
206
210
|
lines -= 2
|
|
207
211
|
|
|
212
|
+
logger.debug("initial available lines in screen = %d", lines)
|
|
208
213
|
# Allocate lines to processes that have a fixed percentage of lines set
|
|
209
214
|
allocated_process_lines = lines // len(output.processes)
|
|
215
|
+
logger.debug("initial allocated_process_lines = %d", allocated_process_lines)
|
|
210
216
|
processes_with_dynamic_lines: list[ProcessOutput] = []
|
|
211
217
|
used_lines = 0
|
|
212
218
|
for process_output in output.processes:
|
|
@@ -220,6 +226,7 @@ class ConsolePrinter:
|
|
|
220
226
|
|
|
221
227
|
# Remove the used lines from the total available lines
|
|
222
228
|
lines -= used_lines
|
|
229
|
+
logger.debug("available lines after allocating percentage lines = %d", lines)
|
|
223
230
|
|
|
224
231
|
while lines:
|
|
225
232
|
# Calculate how many lines each process should have based on how many processes and lines are left
|
|
@@ -231,9 +238,21 @@ class ConsolePrinter:
|
|
|
231
238
|
# If the number of lines in this process output is less than how many terminal lines we would allocate it,
|
|
232
239
|
# Set it's allocated terminal lines to the exact number of lines in its output and remove this number from
|
|
233
240
|
# the total available terminal lines
|
|
241
|
+
logger.debug(
|
|
242
|
+
"process [%s] lines = %d, allocated = %d",
|
|
243
|
+
process_output.process.command,
|
|
244
|
+
process_output.lines,
|
|
245
|
+
allocated_process_lines,
|
|
246
|
+
)
|
|
234
247
|
if process_output.lines < allocated_process_lines:
|
|
248
|
+
logger.debug(
|
|
249
|
+
"process [%s] lines less than allocated, reducing allocated lines to %s",
|
|
250
|
+
process_output.process.command,
|
|
251
|
+
process_output.lines,
|
|
252
|
+
)
|
|
235
253
|
process_output.process.lines = process_output.lines
|
|
236
254
|
lines -= process_output.process.lines
|
|
255
|
+
logger.debug("new available screen lines = %d", lines)
|
|
237
256
|
recalculate_lines = True
|
|
238
257
|
continue
|
|
239
258
|
|
|
@@ -243,17 +262,23 @@ class ConsolePrinter:
|
|
|
243
262
|
# contains less lines than what we would normally allocate it. This is done so we can allocate these extra lines to the
|
|
244
263
|
# other processes that contain more lines of output.
|
|
245
264
|
if recalculate_lines:
|
|
265
|
+
logger.debug("recalcuting available screen lines")
|
|
246
266
|
processes_with_dynamic_lines = processes_with_excess_output
|
|
247
267
|
else:
|
|
248
268
|
# All remaining processes exceed the number of terminal lines we will allocate them, so allocate them
|
|
249
269
|
# their terminal lines as normal and break out of the while loop
|
|
250
270
|
for process_output in processes_with_excess_output:
|
|
271
|
+
logger.debug(
|
|
272
|
+
"allocating %d lines to process [%s]", allocated_process_lines, process_output.process.command
|
|
273
|
+
)
|
|
251
274
|
process_output.process.lines = allocated_process_lines
|
|
252
275
|
lines -= allocated_process_lines
|
|
276
|
+
logger.debug("new available screen lines = %d", lines)
|
|
253
277
|
|
|
254
278
|
# If there is any lines left, allocate them to the process that currently contains the most lines in its output, or
|
|
255
279
|
# allocate them to the first process if no process contains enough lines
|
|
256
280
|
if lines:
|
|
281
|
+
logger.debug("remaining lines after allocation to all processes = %d", lines)
|
|
257
282
|
process_with_most_lines: ProcessOutput | None = None
|
|
258
283
|
most_lines = 0
|
|
259
284
|
for process_output in output.processes:
|
|
@@ -262,12 +287,26 @@ class ConsolePrinter:
|
|
|
262
287
|
most_lines = process_output.process.lines
|
|
263
288
|
|
|
264
289
|
if not process_with_most_lines:
|
|
265
|
-
|
|
290
|
+
logger.debug(
|
|
291
|
+
"no process found with most output, allocating remaining lines to first process [%s]",
|
|
292
|
+
process_output.process.command,
|
|
293
|
+
)
|
|
294
|
+
process = output.processes[0].process
|
|
295
|
+
process.lines += lines
|
|
296
|
+
logger.debug("process [%s] allocated lines = %d", process.command, process.lines)
|
|
266
297
|
else:
|
|
267
|
-
|
|
298
|
+
logger.debug(
|
|
299
|
+
"found process [%s] with most output, allocating remaining lines",
|
|
300
|
+
process_output.process.command,
|
|
301
|
+
)
|
|
302
|
+
process = process_with_most_lines.process
|
|
303
|
+
process.lines += lines
|
|
304
|
+
logger.debug("process [%s] allocated lines = %d", process.command, process.lines)
|
|
268
305
|
|
|
269
306
|
break
|
|
270
307
|
|
|
308
|
+
logger.debug("all screen lines have been allocated")
|
|
309
|
+
|
|
271
310
|
def get_num_lines(self, line: str, columns: int | None = None) -> int:
|
|
272
311
|
lines = 0
|
|
273
312
|
columns = columns or constants.columns()
|
|
@@ -72,20 +72,6 @@ class Process:
|
|
|
72
72
|
|
|
73
73
|
return buffer
|
|
74
74
|
|
|
75
|
-
def readline(self) -> bytes:
|
|
76
|
-
with self._lock:
|
|
77
|
-
buffer = self._buffer
|
|
78
|
-
if not buffer:
|
|
79
|
-
return b""
|
|
80
|
-
|
|
81
|
-
newline_index = buffer.find(b"\n")
|
|
82
|
-
if newline_index == -1:
|
|
83
|
-
self._buffer = b""
|
|
84
|
-
return buffer
|
|
85
|
-
|
|
86
|
-
self._buffer = buffer[newline_index + 1 :]
|
|
87
|
-
return buffer[: newline_index + 1]
|
|
88
|
-
|
|
89
75
|
def return_code(self) -> int | None:
|
|
90
76
|
return self._process.returncode
|
|
91
77
|
|
|
@@ -108,8 +108,8 @@ class ProcessGroupManager:
|
|
|
108
108
|
return poll
|
|
109
109
|
|
|
110
110
|
def handle_signal(self, signum: int, _frame: Any) -> None:
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
if self._cur_process_group is not None:
|
|
112
|
+
self._cur_process_group.handle_signal(signum)
|
|
113
113
|
|
|
114
114
|
self._exit_code = 128 + signum
|
|
115
115
|
self._interrupt_count += 1
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import signal
|
|
2
|
+
from typing import Generator
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture(autouse=True)
|
|
9
|
+
def mock_signal() -> Generator[MagicMock]:
|
|
10
|
+
# Make sure we mock the signal module so interrupts work normally when running
|
|
11
|
+
# the test suite via pytest
|
|
12
|
+
with patch.object(signal, "signal") as mock:
|
|
13
|
+
yield mock
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import difflib
|
|
2
|
-
import os
|
|
3
2
|
import re
|
|
4
|
-
import signal
|
|
5
|
-
import subprocess
|
|
6
|
-
import time
|
|
7
3
|
from typing import Sequence
|
|
8
4
|
|
|
9
5
|
import pytest
|
|
@@ -135,42 +131,6 @@ class TestInteractiveMode:
|
|
|
135
131
|
],
|
|
136
132
|
)
|
|
137
133
|
|
|
138
|
-
@pytest.mark.parametrize(("signal", "exit_code"), [(signal.SIGINT, 130), (signal.SIGTERM, 143)])
|
|
139
|
-
def test_handles_multiple_signals(self, signal: int, exit_code: int) -> None:
|
|
140
|
-
process = subprocess.Popen(
|
|
141
|
-
[
|
|
142
|
-
"pyallel",
|
|
143
|
-
"./tests/assets/test_handle_multiple_signals.sh",
|
|
144
|
-
],
|
|
145
|
-
env=os.environ.copy(),
|
|
146
|
-
stdout=subprocess.PIPE,
|
|
147
|
-
stderr=subprocess.STDOUT,
|
|
148
|
-
)
|
|
149
|
-
time.sleep(0.3)
|
|
150
|
-
process.send_signal(signal)
|
|
151
|
-
time.sleep(0.1)
|
|
152
|
-
process.send_signal(signal)
|
|
153
|
-
assert process.wait() == exit_code
|
|
154
|
-
|
|
155
|
-
@pytest.mark.parametrize(("signal", "exit_code"), [(signal.SIGINT, 130), (signal.SIGTERM, 143)])
|
|
156
|
-
def test_handles_multiple_signals_with_dependant_commands(self, signal: int, exit_code: int) -> None:
|
|
157
|
-
process = subprocess.Popen(
|
|
158
|
-
[
|
|
159
|
-
"pyallel",
|
|
160
|
-
"./tests/assets/test_handle_multiple_signals.sh",
|
|
161
|
-
":::",
|
|
162
|
-
"./tests/assets/test_handle_multiple_signals.sh",
|
|
163
|
-
],
|
|
164
|
-
env=os.environ.copy(),
|
|
165
|
-
stdout=subprocess.PIPE,
|
|
166
|
-
stderr=subprocess.STDOUT,
|
|
167
|
-
)
|
|
168
|
-
time.sleep(0.3)
|
|
169
|
-
process.send_signal(signal)
|
|
170
|
-
time.sleep(0.1)
|
|
171
|
-
process.send_signal(signal)
|
|
172
|
-
assert process.wait() == exit_code
|
|
173
|
-
|
|
174
134
|
|
|
175
135
|
class TestNonInteractiveMode:
|
|
176
136
|
def test_run_single_command(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
@@ -409,51 +369,3 @@ class TestNonInteractiveMode:
|
|
|
409
369
|
f"[printf hi; sleep {wait}; echo bye] done ✔",
|
|
410
370
|
],
|
|
411
371
|
)
|
|
412
|
-
|
|
413
|
-
@pytest.mark.parametrize(("signal", "exit_code"), [(signal.SIGINT, 130), (signal.SIGTERM, 143)])
|
|
414
|
-
def test_handles_multiple_signals(self, signal: int, exit_code: int) -> None:
|
|
415
|
-
process = subprocess.Popen(
|
|
416
|
-
[
|
|
417
|
-
"pyallel",
|
|
418
|
-
"./tests/assets/test_handle_multiple_signals.sh",
|
|
419
|
-
"-n",
|
|
420
|
-
"-t",
|
|
421
|
-
"--colour",
|
|
422
|
-
"no",
|
|
423
|
-
],
|
|
424
|
-
env=os.environ.copy(),
|
|
425
|
-
stdout=subprocess.PIPE,
|
|
426
|
-
stderr=subprocess.STDOUT,
|
|
427
|
-
)
|
|
428
|
-
time.sleep(0.3)
|
|
429
|
-
process.send_signal(signal)
|
|
430
|
-
time.sleep(0.1)
|
|
431
|
-
process.send_signal(signal)
|
|
432
|
-
assert process.stdout is not None
|
|
433
|
-
out = process.stdout.read()
|
|
434
|
-
assert process.wait() == exit_code, prettify_error(out.decode())
|
|
435
|
-
|
|
436
|
-
@pytest.mark.parametrize(("signal", "exit_code"), [(signal.SIGINT, 130), (signal.SIGTERM, 143)])
|
|
437
|
-
def test_handles_multiple_signals_with_dependant_commands(self, signal: int, exit_code: int) -> None:
|
|
438
|
-
process = subprocess.Popen(
|
|
439
|
-
[
|
|
440
|
-
"pyallel",
|
|
441
|
-
"./tests/assets/test_handle_multiple_signals.sh",
|
|
442
|
-
":::",
|
|
443
|
-
"./tests/assets/test_handle_multiple_signals.sh",
|
|
444
|
-
"-n",
|
|
445
|
-
"-t",
|
|
446
|
-
"--colour",
|
|
447
|
-
"no",
|
|
448
|
-
],
|
|
449
|
-
env=os.environ.copy(),
|
|
450
|
-
stdout=subprocess.PIPE,
|
|
451
|
-
stderr=subprocess.STDOUT,
|
|
452
|
-
)
|
|
453
|
-
time.sleep(0.3)
|
|
454
|
-
process.send_signal(signal)
|
|
455
|
-
time.sleep(0.1)
|
|
456
|
-
process.send_signal(signal)
|
|
457
|
-
assert process.stdout is not None
|
|
458
|
-
out = process.stdout.read()
|
|
459
|
-
assert process.wait() == exit_code, prettify_error(out.decode())
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import subprocess
|
|
4
|
+
from unittest.mock import MagicMock, patch
|
|
4
5
|
|
|
5
6
|
import pytest
|
|
6
7
|
|
|
@@ -58,42 +59,10 @@ def test_from_command_with_lines_modifier_handles_multiple_separators() -> None:
|
|
|
58
59
|
assert process.percentage_lines == 0.5
|
|
59
60
|
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
@patch.object(subprocess, "Popen")
|
|
63
|
+
def test_read(popen_mock: MagicMock) -> None:
|
|
64
|
+
popen_mock.return_value.stdout.read1.side_effect = [b"first\nsecond\n", b""]
|
|
62
65
|
process = Process(1, "echo first; echo second")
|
|
63
66
|
process.run()
|
|
64
|
-
time.sleep(0.01)
|
|
65
67
|
output = process.read()
|
|
66
68
|
assert output == b"first\nsecond\n"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def test_readline() -> None:
|
|
70
|
-
process = Process(1, "echo first; echo second")
|
|
71
|
-
process.run()
|
|
72
|
-
time.sleep(0.01)
|
|
73
|
-
output = process.readline()
|
|
74
|
-
assert output == b"first\n"
|
|
75
|
-
output = process.readline()
|
|
76
|
-
assert output == b"second\n"
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def test_readline_with_read() -> None:
|
|
80
|
-
process = Process(1, "echo first; echo second")
|
|
81
|
-
process.run()
|
|
82
|
-
output = process.readline()
|
|
83
|
-
assert output == b""
|
|
84
|
-
time.sleep(0.01)
|
|
85
|
-
output = process.readline()
|
|
86
|
-
assert output == b"first\n"
|
|
87
|
-
output = process.read()
|
|
88
|
-
assert output == b"second\n"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def test_readline_handles_delayed_newline() -> None:
|
|
92
|
-
process = Process(1, "printf first; sleep 0.1; echo second")
|
|
93
|
-
process.run()
|
|
94
|
-
time.sleep(0.01)
|
|
95
|
-
output = process.readline()
|
|
96
|
-
assert output == b"first"
|
|
97
|
-
time.sleep(0.3)
|
|
98
|
-
output = process.readline()
|
|
99
|
-
assert output == b"second\n"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import subprocess
|
|
4
|
+
from unittest.mock import MagicMock, patch
|
|
4
5
|
|
|
5
6
|
import pytest
|
|
6
7
|
|
|
@@ -64,7 +65,9 @@ def test_from_commands_with_lines_modifier_exceeds_100() -> None:
|
|
|
64
65
|
)
|
|
65
66
|
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
@patch.object(subprocess, "Popen")
|
|
69
|
+
def test_stream(popen_mock: MagicMock) -> None:
|
|
70
|
+
popen_mock.return_value.stdout.read1.return_value = b""
|
|
68
71
|
process_group = ProcessGroup(
|
|
69
72
|
id=1,
|
|
70
73
|
processes=[
|
|
@@ -74,7 +77,6 @@ def test_stream() -> None:
|
|
|
74
77
|
],
|
|
75
78
|
)
|
|
76
79
|
process_group.run()
|
|
77
|
-
time.sleep(0.1)
|
|
78
80
|
output = process_group.stream()
|
|
79
81
|
assert len(output.processes) == 3
|
|
80
82
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import signal
|
|
4
|
+
import subprocess
|
|
3
5
|
import time
|
|
6
|
+
from unittest.mock import MagicMock, call, patch
|
|
4
7
|
|
|
5
8
|
import pytest
|
|
6
9
|
|
|
@@ -10,7 +13,10 @@ from pyallel.process_group import ProcessGroup
|
|
|
10
13
|
from pyallel.process_group_manager import ProcessGroupManager
|
|
11
14
|
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
@patch.object(subprocess, "Popen")
|
|
17
|
+
def test_stream(popen_mock: MagicMock) -> None:
|
|
18
|
+
popen_mock.return_value.stdout.read1.return_value = b""
|
|
19
|
+
popen_mock.return_value.poll.return_value = 0
|
|
14
20
|
pg_manager = ProcessGroupManager(
|
|
15
21
|
process_groups=[
|
|
16
22
|
ProcessGroup(
|
|
@@ -31,7 +37,6 @@ def test_stream() -> None:
|
|
|
31
37
|
)
|
|
32
38
|
pg_manager.run()
|
|
33
39
|
pg_manager.get_cur_process_group_output()
|
|
34
|
-
time.sleep(0.1)
|
|
35
40
|
output = pg_manager.stream()
|
|
36
41
|
assert len(output.process_group_outputs) == 1
|
|
37
42
|
assert output.process_group_outputs[1].id == 1
|
|
@@ -39,7 +44,6 @@ def test_stream() -> None:
|
|
|
39
44
|
assert pg_manager.poll() == 0
|
|
40
45
|
pg_manager.run()
|
|
41
46
|
pg_manager.get_cur_process_group_output()
|
|
42
|
-
time.sleep(0.1)
|
|
43
47
|
output = pg_manager.stream()
|
|
44
48
|
assert len(output.process_group_outputs) == 1
|
|
45
49
|
assert output.process_group_outputs[2].id == 2
|
|
@@ -47,8 +51,8 @@ def test_stream() -> None:
|
|
|
47
51
|
assert pg_manager.poll() == 0
|
|
48
52
|
|
|
49
53
|
|
|
50
|
-
def test_from_args() -> None:
|
|
51
|
-
|
|
54
|
+
def test_from_args(mock_signal: MagicMock) -> None:
|
|
55
|
+
expected_pg_manager = ProcessGroupManager(
|
|
52
56
|
process_groups=[
|
|
53
57
|
ProcessGroup(
|
|
54
58
|
id=1,
|
|
@@ -59,18 +63,26 @@ def test_from_args() -> None:
|
|
|
59
63
|
)
|
|
60
64
|
]
|
|
61
65
|
)
|
|
62
|
-
|
|
63
|
-
assert len(
|
|
66
|
+
pg_manager = ProcessGroupManager.from_args("sleep 0.1", "::", "sleep 0.2")
|
|
67
|
+
assert len(pg_manager._process_groups) == len(expected_pg_manager._process_groups)
|
|
64
68
|
|
|
65
69
|
for pg1, pg2 in zip(
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
expected_pg_manager._process_groups,
|
|
71
|
+
pg_manager._process_groups,
|
|
68
72
|
):
|
|
69
73
|
assert len(pg1.processes) == len(pg2.processes)
|
|
70
74
|
|
|
75
|
+
assert mock_signal.call_count == 2
|
|
76
|
+
mock_signal.assert_has_calls(
|
|
77
|
+
[
|
|
78
|
+
call(signal.SIGINT, pg_manager.handle_signal),
|
|
79
|
+
call(signal.SIGTERM, pg_manager.handle_signal),
|
|
80
|
+
]
|
|
81
|
+
)
|
|
82
|
+
|
|
71
83
|
|
|
72
84
|
@pytest.mark.parametrize(
|
|
73
|
-
("args", "
|
|
85
|
+
("args", "expected_pg_manager"),
|
|
74
86
|
[
|
|
75
87
|
(
|
|
76
88
|
["sleep 0.1", ":::", "sleep 0.2", "::", "sleep 0.3", ":::", "sleep 0.4"],
|
|
@@ -171,13 +183,13 @@ def test_from_args() -> None:
|
|
|
171
183
|
),
|
|
172
184
|
],
|
|
173
185
|
)
|
|
174
|
-
def test_from_args_with_separators(args: list[str],
|
|
175
|
-
|
|
176
|
-
assert len(
|
|
186
|
+
def test_from_args_with_separators(args: list[str], expected_pg_manager: ProcessGroupManager) -> None:
|
|
187
|
+
pg_manager = ProcessGroupManager.from_args(*args)
|
|
188
|
+
assert len(pg_manager._process_groups) == len(expected_pg_manager._process_groups)
|
|
177
189
|
|
|
178
190
|
for pg1, pg2 in zip(
|
|
179
|
-
|
|
180
|
-
|
|
191
|
+
expected_pg_manager._process_groups,
|
|
192
|
+
pg_manager._process_groups,
|
|
181
193
|
):
|
|
182
194
|
assert len(pg1.processes) == len(pg2.processes)
|
|
183
195
|
|
|
@@ -188,3 +200,37 @@ def test_from_args_with_bad_separator() -> None:
|
|
|
188
200
|
match=r"no commands provided for process group 1, did you forgot to provide them before the ::: symbol?",
|
|
189
201
|
):
|
|
190
202
|
ProcessGroupManager.from_args(":::", "echo hi")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@patch.object(subprocess, "Popen")
|
|
206
|
+
def test_handle_signal(popen_mock: MagicMock) -> None:
|
|
207
|
+
popen_mock.return_value.stdout.read1.side_effect = lambda _: time.sleep(1)
|
|
208
|
+
pg_manager = ProcessGroupManager.from_args("sleep 0.1", "::", "sleep 0.2")
|
|
209
|
+
pg_manager.run()
|
|
210
|
+
|
|
211
|
+
pg_manager.handle_signal(signum=signal.SIGINT, _frame=MagicMock())
|
|
212
|
+
|
|
213
|
+
send_signal_mock: MagicMock = popen_mock.return_value.send_signal
|
|
214
|
+
assert send_signal_mock.call_count == 2
|
|
215
|
+
send_signal_mock.assert_has_calls([call(signal.SIGINT), call(signal.SIGINT)])
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@patch.object(subprocess, "Popen")
|
|
219
|
+
def test_handle_signal_multiple(popen_mock: MagicMock) -> None:
|
|
220
|
+
popen_mock.return_value.stdout.read1.side_effect = lambda _: time.sleep(1)
|
|
221
|
+
pg_manager = ProcessGroupManager.from_args("sleep 0.1", "::", "sleep 0.2")
|
|
222
|
+
pg_manager.run()
|
|
223
|
+
|
|
224
|
+
pg_manager.handle_signal(signum=signal.SIGINT, _frame=MagicMock())
|
|
225
|
+
pg_manager.handle_signal(signum=signal.SIGINT, _frame=MagicMock())
|
|
226
|
+
|
|
227
|
+
send_signal_mock: MagicMock = popen_mock.return_value.send_signal
|
|
228
|
+
assert send_signal_mock.call_count == 4
|
|
229
|
+
send_signal_mock.assert_has_calls(
|
|
230
|
+
[
|
|
231
|
+
call(signal.SIGINT),
|
|
232
|
+
call(signal.SIGINT),
|
|
233
|
+
call(signal.SIGKILL),
|
|
234
|
+
call(signal.SIGKILL),
|
|
235
|
+
]
|
|
236
|
+
)
|
pyallel-2.0.6/.vim/project.shada
DELETED
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|