diff --git a/src/devedeng/error.py b/src/devedeng/error.py
index 1b613b4..57bc8a0 100644
--- a/src/devedeng/error.py
+++ b/src/devedeng/error.py
@@ -14,14 +14,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see
-from gi.repository import Gtk, Gdk
+from gi.repository import Gtk, Gdk, GLib
import os
import devedeng.configuration_data
class error_window:
- def __init__(self):
+ def __init__(self, process=None):
self.config = devedeng.configuration_data.configuration.get_config()
@@ -34,10 +34,60 @@ class error_window:
wdebug_buffer = builder.get_object("debug_buffer")
wdebug_buffer.set_text(self.config.get_log())
+ # lead with a focused summary of what actually failed, so the cause is
+ # visible without digging through the full log in the expander
+ wlabel = builder.get_object("label1")
+ if (wlabel is not None) and (process is not None):
+ wlabel.set_line_wrap(True)
+ wlabel.set_max_width_chars(80)
+ wlabel.set_selectable(True)
+ wlabel.set_markup(error_window.build_summary(process))
+
werror_window.show_all()
werror_window.run()
werror_window.destroy()
+ @staticmethod
+ def build_summary(process):
+ esc = GLib.markup_escape_text
+ parts = ["" + esc(_("The disc could not be created.")) + ""]
+
+ step = getattr(process, "text", "") or ""
+ if step:
+ parts.append("")
+ parts.append(esc(_("Failed step: %s") % step))
+
+ command = getattr(process, "command_var", None)
+ if command:
+ cmd_str = " ".join(str(c) for c in command)
+ if len(cmd_str) > 400:
+ cmd_str = cmd_str[:400] + " ..."
+ parts.append(esc(_("Command: %s") % cmd_str))
+
+ tail = error_window._stderr_tail(process)
+ if tail:
+ parts.append("")
+ parts.append(esc(_("Last messages:")))
+ parts.append("" + esc(tail) + "")
+
+ parts.append("")
+ parts.append(esc(_("See the full log below for details.")))
+ return "\n".join(parts)
+
+ @staticmethod
+ def _stderr_tail(process, max_lines=12, max_line_len=200):
+ raw = getattr(process, "stderr_data", "") or ""
+ if not raw:
+ raw = getattr(process, "stdout_data", "") or ""
+ if not raw:
+ return ""
+ lines = [l.strip() for l in raw.replace("\r", "\n").split("\n")]
+ lines = [l for l in lines if l]
+ tail = lines[-max_lines:]
+ tail = [(l[:max_line_len] + " ...") if len(l) > max_line_len else l
+ for l in tail]
+ return "\n".join(tail)
+
def on_copy_clicked(self, b):
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
diff --git a/src/devedeng/runner.py b/src/devedeng/runner.py
index b548573..9b0ede0 100644
--- a/src/devedeng/runner.py
+++ b/src/devedeng/runner.py
@@ -132,7 +132,7 @@ class runner(GObject.GObject):
for element in self.proc_list:
element.cancel()
self.wprogress.destroy()
- devedeng.error.error_window()
+ devedeng.error.error_window(process)
self.emit("done", 1) # there was an error
return
diff --git a/tests/test_error_summary.py b/tests/test_error_summary.py
new file mode 100644
index 0000000..271b336
--- /dev/null
+++ b/tests/test_error_summary.py
@@ -0,0 +1,74 @@
+"""Tests for the focused failure summary shown in the error window."""
+
+from devedeng.error import error_window
+
+
+class FakeProc:
+ def __init__(self, text="", command_var=None, stderr_data="", stdout_data=""):
+ self.text = text
+ self.command_var = command_var or []
+ self.stderr_data = stderr_data
+ self.stdout_data = stdout_data
+
+
+def test_stderr_tail_takes_last_lines():
+ raw = "\n".join("line %d" % i for i in range(100))
+ tail = error_window._stderr_tail(FakeProc(stderr_data=raw), max_lines=5)
+ assert tail.splitlines() == ["line 95", "line 96", "line 97",
+ "line 98", "line 99"]
+
+
+def test_stderr_tail_handles_carriage_returns():
+ raw = "p 1\rp 2\rp 3\ndone\n"
+ tail = error_window._stderr_tail(FakeProc(stderr_data=raw))
+ assert tail.splitlines()[-2:] == ["p 3", "done"]
+
+
+def test_stderr_tail_skips_blank_lines():
+ raw = "real\n\n \n\nlast\n"
+ assert error_window._stderr_tail(FakeProc(stderr_data=raw)) == "real\nlast"
+
+
+def test_stderr_tail_falls_back_to_stdout():
+ p = FakeProc(stderr_data="", stdout_data="only on stdout\n")
+ assert error_window._stderr_tail(p) == "only on stdout"
+
+
+def test_stderr_tail_truncates_long_lines():
+ raw = "x" * 500
+ tail = error_window._stderr_tail(FakeProc(stderr_data=raw), max_line_len=200)
+ assert tail.endswith(" ...")
+ assert len(tail) <= 210
+
+
+def test_build_summary_includes_step_command_and_tail():
+ p = FakeProc(
+ text="Converting My Movie",
+ command_var=["nice", "-n", "10", "ffmpeg", "-i", "in.mkv", "out.mpg"],
+ stderr_data="some warning\nfatal: invalid data found\n",
+ )
+ s = error_window.build_summary(p)
+ assert "Converting My Movie" in s
+ assert "ffmpeg" in s
+ assert "invalid data found" in s
+ # headline is present and markup is well-formed (bold tag closed)
+ assert "" in s and "" in s
+
+
+def test_build_summary_escapes_markup_special_chars():
+ # a path with & and < must be escaped so set_markup doesn't choke
+ p = FakeProc(text="Converting A & B ",
+ command_var=["ffmpeg", "-i", "a&b.mkv"],
+ stderr_data="error: & worse\n")
+ s = error_window.build_summary(p)
+ assert "&" in s
+ assert "<" in s
+ # the raw unescaped sequences should not appear in dynamic content
+ assert "a&b.mkv" not in s
+
+
+def test_build_summary_without_stderr_still_works():
+ p = FakeProc(text="Creating ISO image", command_var=["mkisofs", "-o", "x"])
+ s = error_window.build_summary(p)
+ assert "Creating ISO image" in s
+ assert "mkisofs" in s