[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