diff --git a/docs/source/changing-output.rst b/docs/source/changing-output.rst new file mode 100644 index 0000000..3a69b76 --- /dev/null +++ b/docs/source/changing-output.rst @@ -0,0 +1,157 @@ +.. Copyright 2023-2026 Tom Meltzer. See the top-level COPYRIGHT file for + details. + +.. _changing_output: + +Changing the Output Mode +======================== + +When debugging across multiple ranks, ``mdb`` can produce a large amount of output. By default, +each rank's output is displayed independently, separated by divider lines. This is called *separate* +mode and is the default behaviour. + +When the output from all ranks is identical, this can be redundant. The *combined* output mode +reduces this verbosity by merging common lines across ranks into a single block, prefixing them +with a combined rank range. + +This tutorial demonstrates the difference between the two modes using the ``set output`` command. + +The Example Program +------------------- + +We will use the ``simple-mpi.exe`` binary from the ``examples/`` directory (see :ref:`quick_start` +for details on compiling it). + +Launching the Debug Target +-------------------------- + +First, launch the program with two processes:: + + $ mdb launch -n 2 -t ./simple-mpi.exe + +This starts the debug server and waits for an ``mdb attach`` connection. + +Separate Output Mode (Default) +------------------------------ + +In separate mode, each rank's output is printed independently. The output from each rank is +prefixed with its rank ID (e.g., ``0:`` or ``1:``) and blocks for different ranks are separated by +a line of asterisks (``****``). + +Create a script file ``script-separate.mdb``:: + + set output separate + broadcast start + info proc + break 15 + continue + list + continue + quit + quit + +Run it with:: + + $ mdb attach -x script-separate.mdb > separate.out + +Here is a shortened excerpt of the output from the ``info proc`` and ``list`` commands: + +**``info proc`` in separate mode:** + +.. code-block:: console + + 0: process 44900 + 0: cmdline = '/home/melt/sync/cambridge/projects/side/mdb/examples/simple-mpi.exe' + 0: cwd = '/home/melt/sync/cambridge/projects/side/mdb' + 0: exe = '/home/melt/sync/cambridge/projects/side/mdb/examples/simple-mpi.exe' + ************************************************************************ + 1: process 44903 + 1: cmdline = '/home/melt/sync/cambridge/projects/side/mdb/examples/simple-mpi.exe' + 1: cwd = '/home/melt/sync/cambridge/projects/side/mdb' + 1: exe = '/home/melt/sync/cambridge/projects/side/mdb/examples/simple-mpi.exe' + +Each rank's output appears in its own block, prefixed with just the rank number, separated by +``****`` dividers. + +**``list`` in separate mode:** + +.. code-block:: console + + 0: 10 + 0: 11 call mpi_init(ierror) + 0: 12 call mpi_comm_size(mpi_comm_world, size_of_cluster, ierror) + 0: 13 call mpi_comm_rank(mpi_comm_world, process_rank, ierror) + 0: 14 + 0: 15 var = 10.*process_rank + 0: 16 + 0: 17 if (process_rank == 0) then + 0: 18 print *, 'process 0 sleeping for 3s...' + 0: 19 do i = 1, 3 + ************************************************************************ + 1: 10 + 1: 11 call mpi_init(ierror) + 1: 12 call mpi_comm_size(mpi_comm_world, size_of_cluster, ierror) + 1: 13 call mpi_comm_rank(mpi_comm_world, process_rank, ierror) + 1: 14 + 1: 15 var = 10.*process_rank + 1: 16 + 1: 17 if (process_rank == 0) then + 1: 18 print *, 'process 0 sleeping for 3s...' + 1: 19 do i = 1, 3 + +Since the source code is identical across ranks, the ``list`` output is duplicated for each rank. + +Combined Output Mode +-------------------- + +In combined mode, lines that are identical across all ranks are merged and prefixed with a combined +rank range (e.g., ``0-1:``). Lines that differ between ranks are still shown per-rank with a single +rank prefix (e.g., `` 0:``). No ``****`` dividers are used. + +Create a script file ``script-combined.mdb``:: + + set output combined + broadcast start + info proc + break 15 + continue + list + continue + quit + quit + +Run it with:: + + $ mdb attach -x script-combined.mdb > combined.out + +**``info proc`` in combined mode:** + +.. code-block:: console + + 0: process 45004 + 0-1: cmdline = '/home/melt/sync/cambridge/projects/side/mdb/examples/simple-mpi.exe' + 0-1: cwd = '/home/melt/sync/cambridge/projects/side/mdb' + 0-1: exe = '/home/melt/sync/cambridge/projects/side/mdb/examples/simple-mpi.exe' + 1: process 45005 + +Notice how the three identical lines (``cmdline``, ``cwd``, ``exe``) are merged under a single +``0-1:`` prefix. The ``process`` line differs between ranks (different PIDs) so it is shown +individually for each rank. + +**``list`` in combined mode:** + +.. code-block:: console + + 0-1: 10 + 0-1: 11 call mpi_init(ierror) + 0-1: 12 call mpi_comm_size(mpi_comm_world, size_of_cluster, ierror) + 0-1: 13 call mpi_comm_rank(mpi_comm_world, process_rank, ierror) + 0-1: 14 + 0-1: 15 var = 10.*process_rank + 0-1: 16 + 0-1: 17 if (process_rank == 0) then + 0-1: 18 print *, 'process 0 sleeping for 3s...' + 0-1: 19 do i = 1, 3 + +Since all ranks share the same source code, every line is identical and merged into a single +compact block. This avoids the duplication seen in separate mode. diff --git a/docs/source/index.rst b/docs/source/index.rst index eccdf0e..93db5f0 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -19,6 +19,7 @@ serial debugger backends e.g., `gdb `_ and `lldb Installation Quick Start + Changing the Output Mode Debugging AMD GPU Kernels .. toctree:: diff --git a/pyproject.toml b/pyproject.toml index be47452..3dce575 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "mdb_debugger" -version = "1.0.6" +version = "1.0.7" dependencies = [ "click==8.1.7", "matplotlib==3.8.3", diff --git a/src/mdb/mdb_shell.py b/src/mdb/mdb_shell.py index 657c2b2..ff26571 100644 --- a/src/mdb/mdb_shell.py +++ b/src/mdb/mdb_shell.py @@ -24,6 +24,7 @@ parse_ranks, pretty_print_response, sort_debug_response, + reduce_response, ) if TYPE_CHECKING: @@ -55,6 +56,7 @@ def __init__(self, shell_opts: ShellOpts, client: Client) -> None: self.exchange_select = parse_ranks(self.exchange_select_str) self.select_str = self.exchange_select_str self.select = self.exchange_select + self.output_mode = "separate" # 'separate' or 'combined' backend_name = shell_opts["backend_name"].lower() if backend_name in backends: self.backend = backends[backend_name]() @@ -214,7 +216,10 @@ def ask_remain_calm(signame: str) -> None: if command_response.msg_type == "exchange_command_response": response = sort_debug_response(command_response.data["results"]) - pretty_print_response(response) + if self.output_mode == "combined": + reduce_response(response) + else: + pretty_print_response(response) else: print("Received unexpected message type: %s", command_response.msg_type) return @@ -250,6 +255,44 @@ def do_shell(self, line: str) -> None: run(split(line)) return + def do_set(self, line: str) -> None: + """ + Description: + Set mdb options. + + Example: + Switch output format between separate and combined mode: + + (mdb) set output combined + (mdb) set output separate + + - separate shows all output for each rank separate by ***'s + - combined reduces common output across the ranks + + Show current settings: + + (mdb) set + """ + if not line: + print(f"output: {self.output_mode}") + return + + parts = line.split() + if len(parts) < 2: + print("Usage: set output [separate|combined]") + return + + if parts[0].lower() == "output": + mode = parts[1].lower() + if mode in ("separate", "combined"): + self.output_mode = mode + else: + print( + f"Error: unknown output mode '{mode}'. Use 'separate' or 'combined'." + ) + else: + print(f"Error: unknown option '{parts[0]}'. Use 'output'.") + def do_select(self, line: str) -> None: """ Description: diff --git a/src/mdb/utils.py b/src/mdb/utils.py index 3ac7f88..de9f34f 100644 --- a/src/mdb/utils.py +++ b/src/mdb/utils.py @@ -4,6 +4,7 @@ import re from os.path import expanduser from typing import TYPE_CHECKING +from collections import defaultdict if TYPE_CHECKING: from .backend import DebugBackend @@ -21,6 +22,29 @@ def sort_debug_response(results: dict[int, str]) -> dict[int, str]: return dict(sorted(results.items())) +def collapse_ranges(values: list[int]) -> str: + """Collapse a list of integers into minimal range notation. + + e.g. [1, 2, 3, 6, 7, 10, 11, 12] -> '1-3,6-7,10-12' + """ + sorted_vals = sorted(set(values)) + if not sorted_vals: + return "" + + ranges = [] + start = end = sorted_vals[0] + + for n in sorted_vals[1:]: + if n == end + 1: + end = n + else: + ranges.append(f"{start}-{end}" if start != end else str(start)) + start = end = n + + ranges.append(f"{start}-{end}" if start != end else str(start)) + return ",".join(ranges) + + def pretty_print_response(response: dict[int, str]) -> None: lines = [] for rank, result in response.items(): @@ -30,6 +54,53 @@ def pretty_print_response(response: dict[int, str]) -> None: print(combined_output) +def reduce_response(response: dict[int, str]) -> None: + """Reduce debug output by deduplicating common lines across ranks. + + Parses each rank's output, groups identical lines together, and prints + each unique line prefixed by the collapsed set of ranks that produced it. + Lines from only one rank appear normally; shared lines are printed once + with all contributing ranks. + + Example output before and after: + + Before (raw output per rank): + + 0: process 45402 + 0: cmdline = '/mdb/examples/simple-mpi.exe' + 0: cwd = '/mdb' + 0: exe = '/mdb/examples/simple-mpi.exe' + ************************************************************************ + 1: process 45403 + 1: cmdline = '/mdb/examples/simple-mpi.exe' + 1: cwd = '/mdb' + 1: exe = '/mdb/examples/simple-mpi.exe' + + After (deduplicated, ranks collapsed): + + 0: process 45402 + 0-1: cmdline = '/mdb/examples/simple-mpi.exe' + 0-1: cwd = '/mdb' + 0-1: exe = '/mdb/examples/simple-mpi.exe' + 1: process 45403 + + Args: + response: dict mapping process rank (int) to its output string. + """ + reduced = defaultdict(list) + for rank, result in response.items(): + if result: + for line in result.split("\r\n")[1:-1]: + reduced[line].append(rank) + + reduced = {k: collapse_ranges(v) for k, v in reduced.items()} + + max_len = max([len(v) for v in reduced.values()], default=0) + for line, ranks_str in reduced.items(): + padded = ranks_str.rjust(max_len) + print(f"{padded}: {line}") + + def extract_float(line: str, backend: "DebugBackend") -> float: float_regex = backend.float_regex line = strip_control_characters(line) @@ -49,9 +120,7 @@ def extract_float(line: str, backend: "DebugBackend") -> float: def prepend_ranks(rank: int, result: str) -> str: - return "".join( - [f"{rank}:\t" + line + "\r\n" for line in result.split("\r\n")[1:-1]] - ) + return "".join([f"{rank}: " + line + "\r\n" for line in result.split("\r\n")[1:-1]]) def strip_bracketted_paste(text: str) -> str: diff --git a/tests/output/answer-gdb.stdout b/tests/output/answer-gdb.stdout index ac72227..22a9a2f 100644 --- a/tests/output/answer-gdb.stdout +++ b/tests/output/answer-gdb.stdout @@ -1,57 +1,57 @@ hello -0:cmdline = simple-mpi.exe -0:cwd = [mdb root] -0:exe = simple-mpi.exe +0: cmdline = simple-mpi.exe +0: cwd = [mdb root] +0: exe = simple-mpi.exe ************************************************************************ -1:cmdline = simple-mpi.exe -1:cwd = [mdb root] -1:exe = simple-mpi.exe -0:Breakpoint 2 at [hex address]: file simple-mpi.f90, line 15. +1: cmdline = simple-mpi.exe +1: cwd = [mdb root] +1: exe = simple-mpi.exe +0: Breakpoint 2 at [hex address]: file simple-mpi.f90, line 15. ************************************************************************ -1:Breakpoint 2 at [hex address]: file simple-mpi.f90, line 15. -0:Breakpoint 3 at [hex address]: file simple-mpi.f90, line 17. +1: Breakpoint 2 at [hex address]: file simple-mpi.f90, line 15. +0: Breakpoint 3 at [hex address]: file simple-mpi.f90, line 17. ************************************************************************ -1:Breakpoint 3 at [hex address]: file simple-mpi.f90, line 17. -0:Continuing. +1: Breakpoint 3 at [hex address]: file simple-mpi.f90, line 17. +0: Continuing. 0: -0:Thread 1 "simple-mpi.exe" hit Breakpoint 2, simple () at simple-mpi.f90:15 -0:15 var = 10.*process_rank +0: Thread 1 "simple-mpi.exe" hit Breakpoint 2, simple () at simple-mpi.f90:15 +0: 15 var = 10.*process_rank ************************************************************************ -1:Continuing. +1: Continuing. 1: -1:Thread 1 "simple-mpi.exe" hit Breakpoint 2, simple () at simple-mpi.f90:15 -1:15 var = 10.*process_rank -0:Continuing. +1: Thread 1 "simple-mpi.exe" hit Breakpoint 2, simple () at simple-mpi.f90:15 +1: 15 var = 10.*process_rank +0: Continuing. 0: -0:Thread 1 "simple-mpi.exe" hit Breakpoint 3, simple () at simple-mpi.f90:17 -0:17 if (process_rank == 0) then -0:#0 simple () at simple-mpi.f90:17 +0: Thread 1 "simple-mpi.exe" hit Breakpoint 3, simple () at simple-mpi.f90:17 +0: 17 if (process_rank == 0) then +0: #0 simple () at simple-mpi.f90:17 ************************************************************************ -1:#0 simple () at simple-mpi.f90:15 +1: #0 simple () at simple-mpi.f90:15 unrecognized command [made-up-command]. Type help to find out list of possible commands. Error: user specified option [select] must be subset of available ranks (check mdb launch command). select = [10] but available ranks are [0-1]. -1:Continuing. +1: Continuing. 1: -1:Thread 1 "simple-mpi.exe" hit Breakpoint 3, simple () at simple-mpi.f90:17 -1:17 if (process_rank == 0) then +1: Thread 1 "simple-mpi.exe" hit Breakpoint 3, simple () at simple-mpi.f90:17 +1: 17 if (process_rank == 0) then File [deliberately-missing-file.mdb] not found. Please check the file exists and try again. -0:25 -************************************************************************ -1:25 -0:Continuing. -0: 1 s... -0: 2 s... -0: 3 s... -0: in level 1 -0: in level 2 -0: internal process: 0 of 2 -0: var = 0.00000000 -************************************************************************ -1:Continuing. -1: in level 1 -1: in level 2 -1: internal process: 1 of 2 -1: var = 10.0000000 +0: 25 +************************************************************************ +1: 25 +0: Continuing. +0: 1 s... +0: 2 s... +0: 3 s... +0: in level 1 +0: in level 2 +0: internal process: 0 of 2 +0: var = 0.00000000 +************************************************************************ +1: Continuing. +1: in level 1 +1: in level 2 +1: internal process: 1 of 2 +1: var = 10.0000000 ************************************************************************ exiting mdb... \ No newline at end of file diff --git a/tests/output/answer-lldb.stdout b/tests/output/answer-lldb.stdout index ca6d5fb..292e036 100644 --- a/tests/output/answer-lldb.stdout +++ b/tests/output/answer-lldb.stdout @@ -1,89 +1,89 @@ hello -0:* thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 1.1 -0: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:24:7 -0: 21 int process_rank, size_of_cluster; -0: 22 float var; -0: 23 -0:-> 24 var = 0.; -0: ^ -0: 25 -0: 26 MPI_Init(NULL, NULL); -0: 27 MPI_Comm_size(MPI_COMM_WORLD, &size_of_cluster); +0: * thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 1.1 +0: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:24:7 +0: 21 int process_rank, size_of_cluster; +0: 22 float var; +0: 23 +0: -> 24 var = 0.; +0: ^ +0: 25 +0: 26 MPI_Init(NULL, NULL); +0: 27 MPI_Comm_size(MPI_COMM_WORLD, &size_of_cluster); ************************************************************************ -1:* thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 1.1 -1: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:24:7 -1: 21 int process_rank, size_of_cluster; -1: 22 float var; -1: 23 -1:-> 24 var = 0.; -1: ^ -1: 25 -1: 26 MPI_Init(NULL, NULL); -1: 27 MPI_Comm_size(MPI_COMM_WORLD, &size_of_cluster); -0:Breakpoint 2: where = simple-mpi-cpp.exe`main + 95 at simple-mpi-cpp.cpp:30:12, address = [hex address] +1: * thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 1.1 +1: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:24:7 +1: 21 int process_rank, size_of_cluster; +1: 22 float var; +1: 23 +1: -> 24 var = 0.; +1: ^ +1: 25 +1: 26 MPI_Init(NULL, NULL); +1: 27 MPI_Comm_size(MPI_COMM_WORLD, &size_of_cluster); +0: Breakpoint 2: where = simple-mpi-cpp.exe`main + 95 at simple-mpi-cpp.cpp:30:12, address = [hex address] ************************************************************************ -1:Breakpoint 2: where = simple-mpi-cpp.exe`main + 95 at simple-mpi-cpp.cpp:30:12, address = [hex address] -0:Breakpoint 3: where = simple-mpi-cpp.exe`main + 127 at simple-mpi-cpp.cpp:32:20, address = [hex address] +1: Breakpoint 2: where = simple-mpi-cpp.exe`main + 95 at simple-mpi-cpp.cpp:30:12, address = [hex address] +0: Breakpoint 3: where = simple-mpi-cpp.exe`main + 127 at simple-mpi-cpp.cpp:32:20, address = [hex address] ************************************************************************ -1:Breakpoint 3: where = simple-mpi-cpp.exe`main + 127 at simple-mpi-cpp.cpp:32:20, address = [hex address] -0:* thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 2.1 -0: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:30:12 -0: 27 MPI_Comm_size(MPI_COMM_WORLD, &size_of_cluster); -0: 28 MPI_Comm_rank(MPI_COMM_WORLD, &process_rank); -0: 29 -0:-> 30 var = 10.*process_rank; -0: ^ -0: 31 -0: 32 if (process_rank == 0){ +1: Breakpoint 3: where = simple-mpi-cpp.exe`main + 127 at simple-mpi-cpp.cpp:32:20, address = [hex address] +0: * thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 2.1 +0: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:30:12 +0: 27 MPI_Comm_size(MPI_COMM_WORLD, &size_of_cluster); +0: 28 MPI_Comm_rank(MPI_COMM_WORLD, &process_rank); +0: 29 +0: -> 30 var = 10.*process_rank; +0: ^ +0: 31 +0: 32 if (process_rank == 0){ ************************************************************************ -1:* thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 2.1 -1: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:30:12 -1: 27 MPI_Comm_size(MPI_COMM_WORLD, &size_of_cluster); -1: 28 MPI_Comm_rank(MPI_COMM_WORLD, &process_rank); -1: 29 -1:-> 30 var = 10.*process_rank; -1: ^ -1: 31 -1: 32 if (process_rank == 0){ -0:* thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 3.1 -0: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:32:20 -0: 29 -0: 30 var = 10.*process_rank; -0: 31 -0:-> 32 if (process_rank == 0){ -0: ^ -0: 34 -0: 35 for (int i = 0; i < 3; ++i) { -0:* thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 3.1 -0: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:32:20 +1: * thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 2.1 +1: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:30:12 +1: 27 MPI_Comm_size(MPI_COMM_WORLD, &size_of_cluster); +1: 28 MPI_Comm_rank(MPI_COMM_WORLD, &process_rank); +1: 29 +1: -> 30 var = 10.*process_rank; +1: ^ +1: 31 +1: 32 if (process_rank == 0){ +0: * thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 3.1 +0: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:32:20 +0: 29 +0: 30 var = 10.*process_rank; +0: 31 +0: -> 32 if (process_rank == 0){ +0: ^ +0: 34 +0: 35 for (int i = 0; i < 3; ++i) { +0: * thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 3.1 +0: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:32:20 ************************************************************************ -1:* thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 2.1 -1: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:30:12 +1: * thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 2.1 +1: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:30:12 unrecognized command [made-up-command]. Type help to find out list of possible commands. -1:* thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 3.1 -1: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:32:20 -1: 29 -1: 30 var = 10.*process_rank; -1: 31 -1:-> 32 if (process_rank == 0){ -1: ^ -1: 34 -1: 35 for (int i = 0; i < 3; ++i) { +1: * thread #1, name = 'simple-mpi-cpp.', stop reason = breakpoint 3.1 +1: frame #0: [hex address] simple-mpi-cpp.exe`main at simple-mpi-cpp.cpp:32:20 +1: 29 +1: 30 var = 10.*process_rank; +1: 31 +1: -> 32 if (process_rank == 0){ +1: ^ +1: 34 +1: 35 for (int i = 0; i < 3; ++i) { File [deliberately-missing-file.mdb] not found. Please check the file exists and try again. -0:(int) 25 +0: (int) 25 ************************************************************************ -1:(int) 25 -0:0 s... -0:1 s... -0:2 s... -0:in level 1 -0:in level 2 -0:internal process: 0 of 2 -0:var = 0 +1: (int) 25 +0: 0 s... +0: 1 s... +0: 2 s... +0: in level 1 +0: in level 2 +0: internal process: 0 of 2 +0: var = 0 ************************************************************************ -1:in level 1 -1:in level 2 -1:internal process: 1 of 2 -1:var = 10 +1: in level 1 +1: in level 2 +1: internal process: 1 of 2 +1: var = 10 ************************************************************************ exiting mdb... \ No newline at end of file diff --git a/tests/test_utils.py b/tests/test_utils.py index 8dc9cac..a9401f0 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,7 +1,16 @@ # Copyright 2023-2026 Tom Meltzer. See the top-level COPYRIGHT file for # details. -from mdb.utils import parse_ranks, strip_bracketted_paste, strip_control_characters +import pytest + +from mdb.utils import ( + collapse_ranges, + parse_ranks, + pretty_print_response, + reduce_response, + strip_bracketted_paste, + strip_control_characters, +) def test_parse_ranks() -> None: @@ -18,6 +27,76 @@ def test_parse_ranks() -> None: assert ranks == [2, 3, 4] +def test_collapse_ranges() -> None: + # consecutive integers collapse to a range + assert collapse_ranges([1, 2, 3, 4]) == "1-4" + + # single integer + assert collapse_ranges([5]) == "5" + + # multiple disjoint ranges + assert collapse_ranges([1, 2, 3, 6, 7, 10, 11, 12]) == "1-3,6-7,10-12" + + # unsorted input is sorted internally + assert collapse_ranges([12, 10, 11, 7, 6, 3, 2, 1]) == "1-3,6-7,10-12" + + # duplicates are deduplicated + assert collapse_ranges([1, 1, 2, 2, 3]) == "1-3" + + # empty list returns empty string + assert collapse_ranges([]) == "" + + # gaps between single values + assert collapse_ranges([1, 3, 5, 7]) == "1,3,5,7" + + # mix of single values and ranges + assert collapse_ranges([1, 2, 5, 6, 7, 10]) == "1-2,5-7,10" + + +def test_pretty_print_response(capsys: pytest.CaptureFixture[str]) -> None: + response = { + 0: "command ran\r\nline 1\r\nline 2\r\n", + 1: "command ran\r\nline 3\r\n", + } + pretty_print_response(response) + captured = capsys.readouterr() + + assert "0: line 1" in captured.out + assert "0: line 2" in captured.out + assert "1: line 3" in captured.out + assert "*" * 72 in captured.out # separator between ranks + # header lines are skipped + assert "command ran" not in captured.out + + +def test_reduce_response(capsys: pytest.CaptureFixture[str]) -> None: + response = { + 0: "header\r\ncommon line\r\nunique to 0\r\n", + 1: "header\r\ncommon line\r\nunique to 1\r\n", + 2: "header\r\ncommon line\r\n", + } + reduce_response(response) + captured = capsys.readouterr() + + # shared line should be prefixed with collapsed rank range + assert "0-2: common line" in captured.out + # unique lines should show only their rank + assert " 0: unique to 0" in captured.out + assert " 1: unique to 1" in captured.out + # header line is skipped + assert "header" not in captured.out + + +def test_reduce_response_single_rank(capsys: pytest.CaptureFixture[str]) -> None: + response = { + 3: "header\r\nonly line\r\n", + } + reduce_response(response) + captured = capsys.readouterr() + + assert "3: only line" in captured.out + + def test_strip_functions() -> None: text = "bt\r\n\x1b[?2004l\r#0 \x1b[33msimple\x1b[m () at \x1b[32msimple-mpi.f90\x1b[m:1\r\n\x1b[?2004h" text = strip_bracketted_paste(text)