Skip to content

Commit f840195

Browse files
committed
qml: surface debug log open failures in the UI
DebugLogModel::openLogFile() already detects when the debug.log cannot be opened (a missing file, or no application associated with the file type): it sets an openError message, emits openErrorChanged, and returns false. But SettingsDebugLog discarded the return value and never read openError, so a failed open gave the user no feedback at all. Bind the model's openError to an error label on the debug log page, the same way the wallet backup page surfaces its open failure, so the user sees why the log did not open. Cover it both ways: a QML test (with a debug log model stub) checks the error label shows on a failed open and clears on a later success, and a C++ unit test checks the model's missing-file path returns false and sets openError.
1 parent 77ccd91 commit f840195

4 files changed

Lines changed: 136 additions & 3 deletions

File tree

qml/pages/settings/SettingsDebugLog.qml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,20 @@ Page {
101101
}
102102
spacing: 0
103103

104+
CoreText {
105+
objectName: "debugLogOpenErrorText"
106+
Layout.fillWidth: true
107+
Layout.bottomMargin: text.length > 0 ? 10 : 0
108+
visible: text.length > 0
109+
text: debugLogModel.openError
110+
color: Theme.color.red
111+
font.pixelSize: 13
112+
wrapMode: Text.WordWrap
113+
horizontalAlignment: Text.AlignHCenter
114+
Accessible.role: Accessible.StaticText
115+
Accessible.name: text
116+
}
117+
104118
RowLayout {
105119
id: searchRow
106120
objectName: "debugLogSearchRow"

test/qml/qml_tests_main.cpp

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3159,7 +3159,7 @@ class MockDebugLogModel : public QAbstractListModel
31593159
m_filter = filter;
31603160
Q_EMIT filterChanged();
31613161
}
3162-
QString openError() const { return {}; }
3162+
QString openError() const { return m_open_error; }
31633163
int loadMoreCalls() const { return m_load_more_calls; }
31643164

31653165
Q_INVOKABLE void refresh(bool = false) {}
@@ -3170,7 +3170,29 @@ class MockDebugLogModel : public QAbstractListModel
31703170
appendRowsForTest(20);
31713171
setHasMoreLinesForTest(false);
31723172
}
3173-
Q_INVOKABLE bool openLogFile() { return true; }
3173+
Q_INVOKABLE bool openLogFile()
3174+
{
3175+
const QString next = m_open_result ? QString{} : m_pending_error;
3176+
if (m_open_error != next) {
3177+
m_open_error = next;
3178+
Q_EMIT openErrorChanged();
3179+
}
3180+
return m_open_result;
3181+
}
3182+
Q_INVOKABLE void setOpenLogFileResult(bool ok, const QString& error)
3183+
{
3184+
m_open_result = ok;
3185+
m_pending_error = error;
3186+
}
3187+
Q_INVOKABLE void reset()
3188+
{
3189+
m_open_result = true;
3190+
m_pending_error.clear();
3191+
if (!m_open_error.isEmpty()) {
3192+
m_open_error.clear();
3193+
Q_EMIT openErrorChanged();
3194+
}
3195+
}
31743196
Q_INVOKABLE void updateRelativeTimes() {}
31753197

31763198
Q_INVOKABLE void resetForTest(int count, bool has_more_lines)
@@ -3308,6 +3330,9 @@ class MockDebugLogModel : public QAbstractListModel
33083330
int m_load_more_calls{0};
33093331
int m_next_new_row{0};
33103332
int m_next_old_row{0};
3333+
QString m_open_error;
3334+
QString m_pending_error;
3335+
bool m_open_result{true};
33113336
};
33123337

33133338
class MockClipboard : public QObject
@@ -3401,6 +3426,7 @@ public Q_SLOTS:
34013426
engine->rootContext()->setContextProperty(QStringLiteral("optionsModel"), &options_model);
34023427
engine->rootContext()->setContextProperty(QStringLiteral("chainModel"), &chain_model);
34033428
engine->rootContext()->setContextProperty(QStringLiteral("nodeModel"), &node_model);
3429+
engine->rootContext()->setContextProperty(QStringLiteral("debugLogModel"), &debug_log_model);
34043430
engine->rootContext()->setContextProperty(QStringLiteral("peerTableModel"), &peer_table_model);
34053431
engine->rootContext()->setContextProperty(QStringLiteral("networkTrafficTower"), &network_traffic_tower);
34063432
engine->rootContext()->setContextProperty(QStringLiteral("testNetworkTrafficTower"), &network_traffic_tower);
@@ -3420,7 +3446,6 @@ public Q_SLOTS:
34203446
engine->rootContext()->setContextProperty(QStringLiteral("testCoinsListModel"), &coins_list_model);
34213447
engine->rootContext()->setContextProperty(QStringLiteral("testBumpModel"), &bump_model);
34223448
engine->rootContext()->setContextProperty(QStringLiteral("desktopWindowBehaviorModel"), &desktop_window_behavior_model);
3423-
engine->rootContext()->setContextProperty(QStringLiteral("debugLogModel"), &debug_log_model);
34243449
engine->rootContext()->setContextProperty(QStringLiteral("testDebugLogModel"), &debug_log_model);
34253450
engine->addImportPath(QStringLiteral(BITCOINQML_QML_SOURCE_DIR));
34263451
}

test/qml/tst_debuglog.qml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) 2026 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
import QtQuick 2.15
6+
import QtTest 1.2
7+
import "../../qml/pages/settings"
8+
9+
TestCase {
10+
name: "DebugLog"
11+
when: windowShown
12+
width: 520
13+
height: 720
14+
15+
Component {
16+
id: debugLogComponent
17+
18+
SettingsDebugLog {
19+
width: 480
20+
height: 680
21+
}
22+
}
23+
24+
function init() {
25+
debugLogModel.reset()
26+
}
27+
28+
function createPage() {
29+
const page = createTemporaryObject(debugLogComponent, this)
30+
verify(page !== null)
31+
return page
32+
}
33+
34+
// A failed open surfaces the model's error message on the page. The label
35+
// is hidden (text bound to the empty openError) until the open fails.
36+
function test_open_failure_shows_error() {
37+
const page = createPage()
38+
const errorText = findChild(page, "debugLogOpenErrorText")
39+
verify(errorText !== null)
40+
compare(errorText.text, "")
41+
42+
debugLogModel.setOpenLogFileResult(false, "Could not open debug log file.")
43+
debugLogModel.openLogFile()
44+
45+
compare(errorText.text, "Could not open debug log file.")
46+
}
47+
48+
// A later successful open clears the previously shown error.
49+
function test_successful_open_clears_error() {
50+
const page = createPage()
51+
const errorText = findChild(page, "debugLogOpenErrorText")
52+
verify(errorText !== null)
53+
54+
debugLogModel.setOpenLogFileResult(false, "Could not open debug log file.")
55+
debugLogModel.openLogFile()
56+
compare(errorText.text, "Could not open debug log file.")
57+
58+
debugLogModel.setOpenLogFileResult(true, "")
59+
debugLogModel.openLogFile()
60+
61+
compare(errorText.text, "")
62+
}
63+
}

test/test_debuglogmodel.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ private Q_SLOTS:
6969
void filter_updatesIncrementallyAndWhileInactive();
7070
void rotation_fallsBackToFullSnapshot();
7171
void loadLimit_changesKeepRetainedCacheBounded();
72+
void openErrorEmptyByDefault();
73+
void openMissingFileReportsError();
7274
};
7375

7476
void DebugLogModelTests::inactiveModel_ignoresRefreshUntilActivated()
@@ -607,6 +609,35 @@ void DebugLogModelTests::loadLimit_changesKeepRetainedCacheBounded()
607609
QCOMPARE(insert_spy.at(0).at(2).toInt(), 3);
608610
}
609611

612+
void DebugLogModelTests::openErrorEmptyByDefault()
613+
{
614+
QTemporaryDir temp_dir;
615+
QVERIFY(temp_dir.isValid());
616+
const fs::path log_path = fs::PathFromString(temp_dir.filePath("debug.log").toStdString());
617+
618+
DebugLogModel model(log_path);
619+
QVERIFY(model.openError().isEmpty());
620+
}
621+
622+
// openLogFile() must report the failure rather than silently returning when the
623+
// log file cannot be opened, so the UI can surface it. A missing file is the
624+
// deterministic failure path; the "no associated application" case (which needs
625+
// a desktop environment) goes through the same set-error/return-false branch.
626+
void DebugLogModelTests::openMissingFileReportsError()
627+
{
628+
QTemporaryDir temp_dir;
629+
QVERIFY(temp_dir.isValid());
630+
// A file that does not exist inside an otherwise-empty temporary directory.
631+
const fs::path missing = fs::PathFromString(temp_dir.filePath("does-not-exist.log").toStdString());
632+
633+
DebugLogModel model(missing);
634+
QSignalSpy error_spy(&model, &DebugLogModel::openErrorChanged);
635+
636+
QVERIFY(!model.openLogFile());
637+
QVERIFY(!model.openError().isEmpty());
638+
QCOMPARE(error_spy.count(), 1);
639+
}
640+
610641
#ifdef BITCOINQML_NO_TEST_MAIN
611642
#include <test/qt_test_registry.h>
612643
BITCOINQML_REGISTER_QT_TEST(DebugLogModelTests)

0 commit comments

Comments
 (0)