[education/rkward] rkward: Enhance test case, debug messages; document the problem (no fix, yet)
Thomas Friedrichsmeier
null at kde.org
Sun Aug 31 13:51:37 BST 2025
Git commit 5cc12cbbe7c55edf8a4abc5948ecbe4f5ca5623c by Thomas Friedrichsmeier.
Committed on 31/08/2025 at 08:45.
Pushed by tfry into branch 'master'.
Enhance test case, debug messages; document the problem (no fix, yet)
M +16 -0 rkward/autotests/core_test.cpp
M +20 -9 rkward/rbackend/rkrbackend.cpp
M +1 -0 rkward/rbackend/rksignalsupport.cpp
M +6 -2 rkward/settings/rksettingsmodulewatch.cpp
M +6 -1 rkward/settings/rksettingsmodulewatch.h
https://invent.kde.org/education/rkward/-/commit/5cc12cbbe7c55edf8a4abc5948ecbe4f5ca5623c
diff --git a/rkward/autotests/core_test.cpp b/rkward/autotests/core_test.cpp
index 44e5fea8f..9b3c629b8 100644
--- a/rkward/autotests/core_test.cpp
+++ b/rkward/autotests/core_test.cpp
@@ -38,6 +38,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
#include "../rkward.h"
#include "../settings/rksettings.h"
#include "../settings/rksettingsmodulekateplugins.h"
+#include "../settings/rksettingsmodulewatch.h"
#include "../version.h"
#include "../windows/katepluginintegration.h"
@@ -65,6 +66,19 @@ void RKDebug(int, int level, const char *fmt, ...) {
va_end(ap);
}
+class ScopeHandler {
+ public:
+ ScopeHandler(std::function<void()> setup, std::function<void()> cleanup) : cleanup(cleanup) {
+ setup();
+ };
+ ~ScopeHandler() {
+ cleanup();
+ }
+
+ private:
+ std::function<void()> cleanup;
+};
+
/** This test suite sets up a mostly complete application. That's a bit heavy, but arguably, a) modularity isn't ideal in RKWard, and b) many of the more interesting
* tests involve passing commands to the R backend, and then verifying the expected state in the frontend. That alone requires a fairly extensive setup, anyway.
*
@@ -420,6 +434,8 @@ class RKWardCoreTest : public QObject {
}
void priorityCommandTest() {
+ // This test runs much faster when silencing the log window. Running faster also seems to help triggering the bug.
+ ScopeHandler sup([]() { RKSettingsModuleWatch::forTestingSuppressOutput(true); }, []() { RKSettingsModuleWatch::forTestingSuppressOutput(false); });
// NOTE: Hopefully, this test case is fixed for good, but when it is not (it wasn't quite in 08/2025), 10 iterations are not near enough to trigger the
// failure, somewhat reliably. On my test system, I rather needed on the order to 10000 iterations for that. Of course that makes testing a pain, and cannot
// reasonably be done on the CI...
diff --git a/rkward/rbackend/rkrbackend.cpp b/rkward/rbackend/rkrbackend.cpp
index 1c3e7045c..6360af5d2 100644
--- a/rkward/rbackend/rkrbackend.cpp
+++ b/rkward/rbackend/rkrbackend.cpp
@@ -85,6 +85,7 @@ void RKRBackend::scheduleInterrupt() {
if (RKRBackendProtocolBackend::inRThread()) {
RK_scheduleIntr();
} else {
+ RK_DEBUG(RBACKEND, DL_DEBUG, "Interrupt scheduled from outside R thread");
#ifdef Q_OS_WIN
RK_scheduleIntr(); // Thread-safe on windows?!
#else
@@ -98,10 +99,21 @@ void RKRBackend::interruptCommand(int command_id) {
RK_DEBUG(RBACKEND, DL_DEBUG, "Received interrupt request for command id %d", command_id);
QMutexLocker lock(&all_current_commands_mutex);
+ /** A request to interrupt a command may happen at very different points in the code:
+ * 0) before it was even sent from the frontend -> frontend will not even send it to the backend code
+ * 1) after is was sent from the frontend, but before we properly started handling it in the backend (deserializing the request, pushing it onto
+ * all_current_commands, feeding it into the REPL/eval) TODO: buggy!
+ * 2) when it is properly running in the backend -> this is the typical case, we worry about
+ * 2b) when it is properly running in the backend, but there is also an active sub-command, which we should allow to complete
+ * 3) after it finished running in the backend, but the frontend wasn't aware of that, yet TODO: buggy! */
if ((command_id == -1) || (!all_current_commands.isEmpty() && (all_current_commands.last()->id == command_id))) {
- if (!too_late_to_interrupt) {
- RK_DEBUG(RBACKEND, DL_DEBUG, "scheduling interrupt for command id %d", command_id);
+ if (too_late_to_interrupt) {
+ RK_DEBUG(RBACKEND, DL_DEBUG, "too late to interrupt command id %d (repl uc status: %d)", command_id, repl_status.user_command_status);
+ } else {
+ RK_DEBUG(RBACKEND, DL_DEBUG, "scheduling interrupt for command id %d (repl uc status: %d)", command_id, repl_status.user_command_status);
+ lock.unlock();
scheduleInterrupt();
+ return;
}
} else {
bool any_found = false;
@@ -115,7 +127,7 @@ void RKRBackend::interruptCommand(int command_id) {
}
}
}
- if (any_found) {
+ if (!any_found) {
RK_DEBUG(RBACKEND, DL_ERROR, "interrupt scheduled for command id %d, but it is not current", command_id);
}
}
@@ -233,16 +245,14 @@ int RReadConsole(const char *prompt, unsigned char *buf, int buflen, int hist) {
}
} else if (RKRBackend::repl_status.user_command_status == RKRBackend::RKReplStatus::UserCommandTransmitted) {
if (RKRBackend::repl_status.user_command_completely_transmitted) {
- // fully transmitted, but R is still asking for more? This looks like an incomplete statement.
+ // fully transmitted, but R is still asking for more, without signalling R_Busy? This looks like an incomplete statement.
// HOWEVER: It may also have been an empty statement such as " ", so let's check whether the prompt looks like a "continue" prompt
- bool incomplete = false;
if (RKTextCodec::fromNative(prompt) == RKRSupport::SEXPToString(RFn::Rf_GetOption(RFn::Rf_install("continue"), ROb(R_BaseEnv)))) {
- incomplete = true;
+ RKRBackend::this_pointer->current_command->status |= RCommand::Failed | RCommand::ErrorIncomplete;
}
- if (incomplete) RKRBackend::this_pointer->current_command->status |= RCommand::Failed | RCommand::ErrorIncomplete;
RKRBackend::repl_status.user_command_status = RKRBackend::RKReplStatus::ReplIterationKilled;
if (RKRBackend::repl_status.user_command_parsed_up_to <= 0) RKRBackend::this_pointer->startOutputCapture(); // HACK: No capture active, but commandFinished() will try to end one
- RFn::Rf_error(""); // to discard the buffer
+ RFn::Rf_error("%s", ""); // to discard the buffer
} else {
RKTransmitNextUserCommandChunk(buf, buflen);
return 1;
@@ -825,6 +835,7 @@ void doError(const QString &callstring) {
if ((RKRBackend::repl_status.eval_depth == 0) && (!RKRBackend::repl_status.browser_context) && (!RKRBackend::this_pointer->isKilled()) && (RKRBackend::repl_status.user_command_status != RKRBackend::RKReplStatus::ReplIterationKilled) && (RKRBackend::repl_status.user_command_status != RKRBackend::RKReplStatus::NoUserCommand)) {
RKRBackend::repl_status.user_command_status = RKRBackend::RKReplStatus::UserCommandFailed;
+ RK_DEBUG(RBACKEND, DL_DEBUG, "user command failed");
}
if (RKRBackend::repl_status.interrupted) {
// it is unlikely, but possible, that an interrupt signal was received, but the current command failed for some other reason, before processing was actually interrupted. In this case, R_interrupts_pending is not yet cleared.
@@ -1474,7 +1485,7 @@ RCommandProxy *RKRBackend::handleRequest(RBackendRequest *request, bool mayHandl
RK_TRACE(RBACKEND);
RK_ASSERT(request);
- // Seed docs for RBackendRequest for hints to make sense of this mess (and eventually to fix it)
+ // See docs for RBackendRequest for hints to make sense of this mess (and eventually to fix it)
RKRBackendProtocolBackend::instance()->sendRequest(request);
if (request->subcommandrequest) {
diff --git a/rkward/rbackend/rksignalsupport.cpp b/rkward/rbackend/rksignalsupport.cpp
index 03dbea1fc..012add4eb 100644
--- a/rkward/rbackend/rksignalsupport.cpp
+++ b/rkward/rbackend/rksignalsupport.cpp
@@ -36,6 +36,7 @@ struct sigaction default_sigabrt_handler;
rk_sighandler_t r_sigint_handler = nullptr;
void (*new_sigint_handler)(void) = nullptr;
void internal_sigint_handler(int num) {
+ RK_DEBUG(RBACKEND, DL_DEBUG, "calling internal sigint handler");
new_sigint_handler();
signal(num, internal_sigint_handler);
}
diff --git a/rkward/settings/rksettingsmodulewatch.cpp b/rkward/settings/rksettingsmodulewatch.cpp
index 86fe40578..dbeef47b5 100644
--- a/rkward/settings/rksettingsmodulewatch.cpp
+++ b/rkward/settings/rksettingsmodulewatch.cpp
@@ -1,6 +1,6 @@
/*
rksettingsmodulewatch - This file is part of RKWard (https://rkward.kde.org). Created: Thu Aug 26 2004
-SPDX-FileCopyrightText: 2004-2024 by Thomas Friedrichsmeier <thomas.friedrichsmeier at kdemail.net>
+SPDX-FileCopyrightText: 2004-2025 by Thomas Friedrichsmeier <thomas.friedrichsmeier at kdemail.net>
SPDX-FileContributor: The RKWard Team <rkward-devel at kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
@@ -31,11 +31,12 @@ RKConfigValue<int> RKSettingsModuleWatch::app_filter{"app command filter", ShowI
RKConfigValue<int> RKSettingsModuleWatch::sync_filter{"sync command filter", ShowError};
RKConfigValue<int> RKSettingsModuleWatch::user_filter{"user command filter", ShowInput | ShowOutput | ShowError | RaiseWindow};
RKConfigValue<uint> RKSettingsModuleWatch::max_log_lines{"max log lines", 1000};
+int RKSettingsModuleWatch::all_suppressed = 0;
// static
bool RKSettingsModuleWatch::shouldShowInput(RCommand *command) {
RK_TRACE(SETTINGS);
-
+ if (all_suppressed) return false;
if (command->type() & RCommand::EmptyCommand) return false;
if (command->type() & (RCommand::Sync | RCommand::PriorityCommand)) {
@@ -56,6 +57,7 @@ bool RKSettingsModuleWatch::shouldShowInput(RCommand *command) {
bool RKSettingsModuleWatch::shouldShowOutput(RCommand *command) {
RK_TRACE(SETTINGS);
+ if (all_suppressed) return false;
if (command->type() & RCommand::EmptyCommand) return false;
if (!shouldShowInput(command)) return false;
@@ -77,6 +79,7 @@ bool RKSettingsModuleWatch::shouldShowOutput(RCommand *command) {
bool RKSettingsModuleWatch::shouldShowError(RCommand *command) {
RK_TRACE(SETTINGS);
+ if (all_suppressed) return false;
if (command->type() & (RCommand::Sync | RCommand::PriorityCommand)) {
return (sync_filter & ShowError);
} else if (command->type() & RCommand::User) {
@@ -94,6 +97,7 @@ bool RKSettingsModuleWatch::shouldShowError(RCommand *command) {
bool RKSettingsModuleWatch::shouldRaiseWindow(RCommand *command) {
RK_TRACE(SETTINGS);
+ if (all_suppressed) return false;
if (command->type() & (RCommand::Sync | RCommand::PriorityCommand)) {
return (sync_filter & RaiseWindow);
} else if (command->type() & RCommand::User) {
diff --git a/rkward/settings/rksettingsmodulewatch.h b/rkward/settings/rksettingsmodulewatch.h
index e08576a46..60a73ae68 100644
--- a/rkward/settings/rksettingsmodulewatch.h
+++ b/rkward/settings/rksettingsmodulewatch.h
@@ -1,6 +1,6 @@
/*
rksettingsmodulewatch - This file is part of the RKWard project. Created: Thu Aug 26 2004
-SPDX-FileCopyrightText: 2004-2024 by Thomas Friedrichsmeier <thomas.friedrichsmeier at kdemail.net>
+SPDX-FileCopyrightText: 2004-2025 by Thomas Friedrichsmeier <thomas.friedrichsmeier at kdemail.net>
SPDX-FileContributor: The RKWard Team <rkward-devel at kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
@@ -37,6 +37,10 @@ class RKSettingsModuleWatch : public RKSettingsModule {
static uint maxLogLines() { return max_log_lines; };
static RKSettingsModuleWatch *instance() { return _instance; };
+ static void forTestingSuppressOutput(bool suppress) {
+ if (suppress) all_suppressed++;
+ else all_suppressed--;
+ };
private:
friend class RKSettingsPageWatch;
@@ -49,6 +53,7 @@ class RKSettingsModuleWatch : public RKSettingsModule {
static RKConfigValue<int> app_filter;
static RKConfigValue<int> sync_filter;
static RKConfigValue<int> user_filter;
+ static int all_suppressed;
static RKConfigValue<uint> max_log_lines;
static RKSettingsModuleWatch *_instance;
More information about the rkward-tracker
mailing list