fix: report a user-requested stop as stopped rather than failed
Stopping a test shuts down the control socket, so iperf_run_client() returns -1 with "unable to send control message ... Broken pipe". That is the expected consequence of the stop, not a fault, but the bridge read the negative return as a failure — so pressing STOP left the UI showing "Error (exit 1)" above a broken-pipe message. The bridge now consults its own g_stop flag before classifying the outcome, and the distinction travels up as Iperf3Result.stopped so the status line can say "Stopped". A stop is reported with exit code 0: the run ended the way the user asked it to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -251,8 +251,14 @@ static void iperf_cleanup_and_signal(JNIEnv *env,
|
|||||||
retire_test();
|
retire_test();
|
||||||
free_argv(argv, argc);
|
free_argv(argv, argc);
|
||||||
|
|
||||||
|
/* A user-requested stop shuts down the control socket, so
|
||||||
|
* iperf_run_client() returns -1 with "Broken pipe" — expected, not a
|
||||||
|
* failure. Check g_stop so that path is reported as a stop, not an error. */
|
||||||
|
int user_stopped = stopped || g_stop;
|
||||||
|
g_stop = 0;
|
||||||
|
|
||||||
/* Signal result to Java */
|
/* Signal result to Java */
|
||||||
if (stopped) {
|
if (user_stopped) {
|
||||||
signal_output(env, state, "[iPerf JNI] Test was stopped by user.\n");
|
signal_output(env, state, "[iPerf JNI] Test was stopped by user.\n");
|
||||||
} else if (ret < 0) {
|
} else if (ret < 0) {
|
||||||
char errbuf[256];
|
char errbuf[256];
|
||||||
|
|||||||
@@ -54,9 +54,13 @@ class Iperf3Service {
|
|||||||
/// Whether the native layer reported an error during this run.
|
/// Whether the native layer reported an error during this run.
|
||||||
bool _sawError = false;
|
bool _sawError = false;
|
||||||
|
|
||||||
|
/// Whether [stop] was called for the current run.
|
||||||
|
bool _stopRequested = false;
|
||||||
|
|
||||||
/// Cancel any running test.
|
/// Cancel any running test.
|
||||||
Future<void> stop() async {
|
Future<void> stop() async {
|
||||||
if (!_isRunning) return;
|
if (!_isRunning) return;
|
||||||
|
_stopRequested = true;
|
||||||
try {
|
try {
|
||||||
await _channel.invokeMethod('stopIperf');
|
await _channel.invokeMethod('stopIperf');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -83,6 +87,7 @@ class Iperf3Service {
|
|||||||
|
|
||||||
_isRunning = true;
|
_isRunning = true;
|
||||||
_sawError = false;
|
_sawError = false;
|
||||||
|
_stopRequested = false;
|
||||||
_outputBuffer.clear();
|
_outputBuffer.clear();
|
||||||
|
|
||||||
final args = config.toCliArgs();
|
final args = config.toCliArgs();
|
||||||
@@ -157,7 +162,10 @@ class Iperf3Service {
|
|||||||
completer.complete(
|
completer.complete(
|
||||||
Iperf3Result(
|
Iperf3Result(
|
||||||
rawOutput: _transcript(),
|
rawOutput: _transcript(),
|
||||||
exitCode: _sawError ? exitCodeNativeError : 0,
|
// A user-requested stop is a normal outcome, not a failure.
|
||||||
|
exitCode:
|
||||||
|
(_sawError && !_stopRequested) ? exitCodeNativeError : 0,
|
||||||
|
stopped: _stopRequested,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,13 @@ class Iperf3Result {
|
|||||||
final String rawOutput;
|
final String rawOutput;
|
||||||
final int exitCode;
|
final int exitCode;
|
||||||
|
|
||||||
|
/// Whether the run ended because the user asked it to stop, rather than
|
||||||
|
/// finishing its configured duration or failing.
|
||||||
|
final bool stopped;
|
||||||
|
|
||||||
const Iperf3Result({
|
const Iperf3Result({
|
||||||
required this.rawOutput,
|
required this.rawOutput,
|
||||||
required this.exitCode,
|
required this.exitCode,
|
||||||
|
this.stopped = false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,7 +166,10 @@ class _TestRunContentState extends State<_TestRunContent> {
|
|||||||
final (String label, Color color) = switch (vm.state) {
|
final (String label, Color color) = switch (vm.state) {
|
||||||
TestRunState.idle => ('Ready', scheme.onSurfaceVariant),
|
TestRunState.idle => ('Ready', scheme.onSurfaceVariant),
|
||||||
TestRunState.running => ('Running…', scheme.primary),
|
TestRunState.running => ('Running…', scheme.primary),
|
||||||
TestRunState.completed => ('Test complete', scheme.primary),
|
TestRunState.completed => (
|
||||||
|
vm.wasStopped ? 'Stopped' : 'Test complete',
|
||||||
|
vm.wasStopped ? scheme.onSurfaceVariant : scheme.primary,
|
||||||
|
),
|
||||||
TestRunState.error => (
|
TestRunState.error => (
|
||||||
'Error${vm.result != null ? ' (exit ${vm.result!.exitCode})' : ''}',
|
'Error${vm.result != null ? ' (exit ${vm.result!.exitCode})' : ''}',
|
||||||
scheme.error,
|
scheme.error,
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ class TestRunViewModel extends ChangeNotifier {
|
|||||||
/// Whether a test is currently in flight.
|
/// Whether a test is currently in flight.
|
||||||
bool get isRunning => _state == TestRunState.running;
|
bool get isRunning => _state == TestRunState.running;
|
||||||
|
|
||||||
|
/// Whether the most recent finished run ended because the user stopped it.
|
||||||
|
bool get wasStopped => _result?.stopped ?? false;
|
||||||
|
|
||||||
/// Start a test with the given configuration.
|
/// Start a test with the given configuration.
|
||||||
Future<void> runTest(TestConfiguration config) async {
|
Future<void> runTest(TestConfiguration config) async {
|
||||||
if (_state == TestRunState.running) return;
|
if (_state == TestRunState.running) return;
|
||||||
|
|||||||
@@ -11,5 +11,15 @@ void main() {
|
|||||||
expect(result.rawOutput, equals('Test complete'));
|
expect(result.rawOutput, equals('Test complete'));
|
||||||
expect(result.exitCode, equals(0));
|
expect(result.exitCode, equals(0));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('stopped defaults to false', () {
|
||||||
|
const result = Iperf3Result(rawOutput: 'x', exitCode: 0);
|
||||||
|
expect(result.stopped, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stopped is preserved when set', () {
|
||||||
|
const result = Iperf3Result(rawOutput: 'x', exitCode: 0, stopped: true);
|
||||||
|
expect(result.stopped, isTrue);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,31 @@ void main() {
|
|||||||
await run;
|
await run;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('wasStopped reflects a user-requested stop', () async {
|
||||||
|
final mock = _MockIperf3Service(
|
||||||
|
onRun: (_) async => const Iperf3Result(
|
||||||
|
rawOutput: 'stopped early',
|
||||||
|
exitCode: 0,
|
||||||
|
stopped: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final vm = TestRunViewModel(iperf3Service: mock);
|
||||||
|
await vm.runTest(const TestConfiguration(server: '192.168.1.1'));
|
||||||
|
// A user stop is a normal outcome: completed, not error.
|
||||||
|
expect(vm.state, equals(TestRunState.completed));
|
||||||
|
expect(vm.wasStopped, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('wasStopped is false for a run that finished on its own', () async {
|
||||||
|
final mock = _MockIperf3Service(
|
||||||
|
onRun: (_) async =>
|
||||||
|
const Iperf3Result(rawOutput: 'done', exitCode: 0),
|
||||||
|
);
|
||||||
|
final vm = TestRunViewModel(iperf3Service: mock);
|
||||||
|
await vm.runTest(const TestConfiguration(server: '192.168.1.1'));
|
||||||
|
expect(vm.wasStopped, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
test('a second runTest while running is ignored', () async {
|
test('a second runTest while running is ignored', () async {
|
||||||
final mock = _MockIperf3Service();
|
final mock = _MockIperf3Service();
|
||||||
final gate = Completer<Iperf3Result>();
|
final gate = Completer<Iperf3Result>();
|
||||||
|
|||||||
Reference in New Issue
Block a user