mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-10-19 16:17:03 +00:00
* test: remove tests that do not pin user-visible behavior Apply the AGENTS.md test rules to the existing Rust and Flutter suites. A test is kept when it is the only coverage of user- or API-visible behavior, the regression test of a reported or fixed bug, a security boundary (one test per rejection class), or a check against an external reference. Tests that assert private state or the order of internal steps, pin review-round corner cases, or repeat what a kept test already covers are removed. Rust: 679 -> 298 tests. Dart: 118 -> 45 tests. Test-only helpers left without callers go with them (applyLastOnlineStateForTesting, terminalKeyboardRowWidth, debugBufferedInputCount, tokio's test-util dev feature), and cfg gates and visibility that existed only for the deleted tests are narrowed. Runtime behavior is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WS1HTjp6UQuG43KEcQPq1R * test: keep tests that pin security and user-visible behavior Restore six tests that review showed are the only coverage of their behavior: - update metadata rejects an artifact file name with path components; - ICE candidates from another session are dropped; - KCP delivers every multi-segment frame queued before a close; - set_files rejects an empty name in a multi-file transfer; - a lopsided elongated cursor keeps its centre hotspot (#16242); - toggling Show hidden during a pending same-path read issues a new read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WS1HTjp6UQuG43KEcQPq1R * test: keep offline update and key-exchange downgrade rejections Restore three tests that are the only coverage of their rejection class: - offline update metadata rejects a signed artifact with an http or credential-bearing URL; - offline verification rejects a renamed local file and a version that does not match the release; - a key-exchange pick lowered in transit leaves both sides unable to decrypt, so a v0 fallback after a failed v1 decrypt cannot pass unnoticed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WS1HTjp6UQuG43KEcQPq1R * test: keep the decomposed-grapheme and reconnect-cancel tests Each catches a plausible wrong change that passes every kept test: mapping the first scalar under a locked Ctrl turns é into \x05, and a reconnect that does not fail pending reads leaves them hanging until the 30 s timeout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WS1HTjp6UQuG43KEcQPq1R --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
226 lines
7.6 KiB
Dart
226 lines
7.6 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_custom_cursor/flutter_custom_cursor.dart';
|
|
import 'package:flutter_hbb/common.dart';
|
|
import 'package:flutter_hbb/generated_bridge.dart' show CursorShape;
|
|
import 'package:flutter_hbb/models/model.dart';
|
|
import 'package:flutter_hbb/native/custom_cursor.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
/// Stands in for the core, which keeps every shape the peer sent.
|
|
class _Cursor extends CursorModel {
|
|
_Cursor(FFI ffi) : super(WeakReference(ffi));
|
|
final core = <String, CursorShape>{};
|
|
final fetched = <String>[];
|
|
@override
|
|
Future<CursorShape?> fetchCursorShape(String id) {
|
|
fetched.add(id);
|
|
return Future.value(core[id]);
|
|
}
|
|
|
|
@override
|
|
bool get showsRemoteCursor => false;
|
|
}
|
|
|
|
String? _key(MouseCursor cursor) =>
|
|
cursor is FlutterCustomMemoryImageCursor ? cursor.key : null;
|
|
|
|
class _FFI extends Fake implements FFI {
|
|
_FFI() {
|
|
ffiModel = FfiModel(WeakReference(this));
|
|
cursorModel = _Cursor(this);
|
|
}
|
|
@override
|
|
final SessionID sessionId = const Uuid().v4obj();
|
|
@override
|
|
late final FfiModel ffiModel;
|
|
@override
|
|
late final CursorModel cursorModel;
|
|
_Cursor get cursor => cursorModel as _Cursor;
|
|
}
|
|
|
|
Uint8List _pixels(int size, int seed) =>
|
|
Uint8List.fromList(List.generate(size * size * 4, (i) => (i + seed) % 256));
|
|
|
|
/// A shape as the core delivers it, and keeps it.
|
|
Future<void> _feed(_FFI ffi, String id, {int size = 8, int seed = 0}) {
|
|
final pixels = _pixels(size, seed);
|
|
ffi.cursor.core[id] =
|
|
CursorShape(hotx: 0, hoty: 0, width: size, height: size, colors: pixels);
|
|
return ffi.ffiModel.handleCursorData(id, 0, 0, size, size, pixels);
|
|
}
|
|
|
|
void _select(_FFI ffi, String id) {
|
|
final evt = {'id': id};
|
|
ffi.ffiModel.updateLastCursorId(evt);
|
|
ffi.ffiModel.handleCursorId(evt);
|
|
}
|
|
|
|
Future<void> _settle() async {
|
|
for (var i = 0; i < 2; i++) {
|
|
await Future<void>.delayed(const Duration(milliseconds: 5));
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
final binding = TestWidgetsFlutterBinding.ensureInitialized();
|
|
final channel = Platform.isWindows
|
|
? SystemChannels.mouseCursor
|
|
: const MethodChannel('flutter_custom_cursor');
|
|
final registered = <String>[];
|
|
final deleted = <String>[];
|
|
late _FFI ffi;
|
|
setUp(() {
|
|
registered.clear();
|
|
deleted.clear();
|
|
binding.defaultBinaryMessenger.setMockMethodCallHandler(channel,
|
|
(call) async {
|
|
final args = call.arguments as Map<dynamic, dynamic>;
|
|
if (call.method.startsWith('createCustomCursor')) {
|
|
registered.add(args['name'] as String);
|
|
return args['name'];
|
|
}
|
|
if (call.method.startsWith('deleteCustomCursor')) {
|
|
deleted.add(args['name'] as String);
|
|
}
|
|
return null;
|
|
});
|
|
ffi = _FFI();
|
|
});
|
|
tearDown(() {
|
|
ffi.cursorModel.disposeImages();
|
|
binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, null);
|
|
});
|
|
|
|
test(
|
|
'only the shape in use keeps an image, and a shape its native cursor holds '
|
|
'keeps no pixels', () async {
|
|
final cursor = ffi.cursorModel;
|
|
for (var round = 0; round < 3; round++) {
|
|
for (final (id, seed) in [('1', 1), ('2', 2), ('3', 3)]) {
|
|
await _feed(ffi, id, seed: seed);
|
|
buildCursorOfCache(cursor, 1.0, cursor.cache);
|
|
await _settle();
|
|
}
|
|
}
|
|
expect(ffi.cursorModel.shapeIds, ['3']);
|
|
for (final id in ['1', '2']) {
|
|
expect(ffi.cursorModel.cachedShape(id)!.hasPixels, isFalse);
|
|
expect(ffi.cursorModel.cachedShape(id)!.data, isNull);
|
|
}
|
|
expect(ffi.cursor.fetched, isEmpty);
|
|
});
|
|
|
|
test('the shape in use keeps its pixels, and a new raster is made at once',
|
|
() async {
|
|
final cursor = ffi.cursorModel;
|
|
await _feed(ffi, '1', size: 32);
|
|
buildCursorOfCache(cursor, 1.0, cursor.cache);
|
|
await _settle();
|
|
expect(cursor.cache!.hasPixels, isTrue);
|
|
expect(buildCursorOfCache(cursor, 0.5, cursor.cache),
|
|
isNot(MouseCursor.defer));
|
|
await _settle();
|
|
expect(ffi.cursor.fetched, isEmpty, reason: 'nothing had to be decoded');
|
|
expect(registered.length, 2);
|
|
expect((cursor.cache!.rasterWidth, cursor.cache!.rasterHeight), (16, 16));
|
|
|
|
await _feed(ffi, '2');
|
|
expect(cursor.cachedShape('1')!.hasPixels, isFalse,
|
|
reason: 'switched away from, a shape keeps no pixels');
|
|
_select(ffi, '1');
|
|
expect(buildCursorOfCache(cursor, 0.5, cursor.cache),
|
|
isNot(MouseCursor.defer));
|
|
await _settle();
|
|
expect(registered.length, 2, reason: 'its native cursor is still there');
|
|
expect(ffi.cursor.fetched, isEmpty);
|
|
});
|
|
|
|
test('native cursors stay for the session, every raster of every shape',
|
|
() async {
|
|
// A delete frees nothing on Windows, so none is deleted before the session ends.
|
|
final cursor = ffi.cursorModel;
|
|
for (var i = 0; i < 100; i++) {
|
|
await _feed(ffi, '$i', size: 32);
|
|
buildCursorOfCache(cursor, 1.0, cursor.cache);
|
|
await _settle();
|
|
}
|
|
expect(deleted, isEmpty);
|
|
expect(cursor.cachedKeys.length, 100);
|
|
|
|
_select(ffi, '0');
|
|
expect(_key(buildCursorOfCache(cursor, 1.0, cursor.cache)),
|
|
cursor.nativeKey(cursor.cache!, 1.0));
|
|
await _settle();
|
|
expect(ffi.cursor.fetched, isEmpty, reason: 'its cursor is still there');
|
|
expect(registered.length, 100);
|
|
});
|
|
|
|
test('a new raster of a shape without pixels keeps the cursor shown before',
|
|
() async {
|
|
final cursor = ffi.cursorModel;
|
|
await _feed(ffi, '1', size: 32);
|
|
buildCursorOfCache(cursor, 1.0, cursor.cache);
|
|
await _settle();
|
|
await _feed(ffi, '2', size: 32);
|
|
final shown = _key(buildCursorOfCache(cursor, 1.0, cursor.cache));
|
|
await _settle();
|
|
_select(ffi, '1'); // its native cursor held its pixels, so they went
|
|
expect(_key(buildCursorOfCache(cursor, 0.5, cursor.cache)), shown);
|
|
await _settle();
|
|
expect(ffi.cursor.fetched, ['1']);
|
|
expect(_key(buildCursorOfCache(cursor, 0.5, cursor.cache)),
|
|
cursor.nativeKey(cursor.cache!, 0.5));
|
|
});
|
|
|
|
test('a shape painted again is decoded again, the last one shown meanwhile',
|
|
() async {
|
|
await _feed(ffi, '1', size: 16);
|
|
await _feed(ffi, '2');
|
|
_select(ffi, '1');
|
|
expect(ffi.cursorModel.image?.width, 8, reason: 'shape 2 until 1 is back');
|
|
await _settle();
|
|
expect(ffi.cursor.fetched, ['1']);
|
|
expect(ffi.cursorModel.image?.width, 16);
|
|
expect(ffi.cursorModel.shapeIds, ['1']);
|
|
});
|
|
|
|
test('a moved tab carries the id in use, and its window asks the core',
|
|
() async {
|
|
await _feed(ffi, '1', size: 16);
|
|
await _feed(ffi, '2');
|
|
await _feed(ffi, '1', size: 16);
|
|
final carried = ffi.ffiModel.cachedPeerData.toString();
|
|
expect(carried, isNot(contains('colors')));
|
|
expect(CachedPeerData.fromString(carried)!.lastCursorId['id'], '1',
|
|
reason: 'a shape arriving is the shape in use');
|
|
|
|
final moved = _FFI();
|
|
addTearDown(moved.cursorModel.disposeImages);
|
|
moved.cursor.core.addAll(ffi.cursor.core);
|
|
_select(moved, '1');
|
|
await _settle();
|
|
expect(moved.cursor.fetched, ['1']);
|
|
expect(moved.cursorModel.cache?.id, '1');
|
|
expect(moved.cursorModel.image?.width, 16);
|
|
});
|
|
|
|
test('a tab closing does not delete the cursors of another tab', () async {
|
|
final other = _FFI();
|
|
addTearDown(other.cursorModel.disposeImages);
|
|
while (preDefaultCursor.cache == null) {
|
|
await _settle();
|
|
}
|
|
buildCursorOfCache(ffi.cursorModel, 1.0, preDefaultCursor.cache);
|
|
buildCursorOfCache(other.cursorModel, 1.0, preDefaultCursor.cache);
|
|
await _settle();
|
|
expect(registered.toSet().length, 2, reason: 'each tab has its own');
|
|
|
|
ffi.cursorModel.clear();
|
|
await _settle();
|
|
expect(other.cursorModel.cachedKeys.single, isNot(isIn(deleted)));
|
|
});
|
|
}
|