Files
kuaishai2/test/device_protocol_test.dart

257 lines
8.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:kuaishai2/features/device/models/device_state.dart';
import 'package:kuaishai2/features/device/services/device_log.dart';
import 'package:kuaishai2/features/device/services/device_message.dart';
import 'package:kuaishai2/features/device/services/json_protocol.dart';
import 'package:kuaishai2/features/device/services/task_payload.dart';
import 'package:kuaishai2/features/programs/models/program.dart';
import 'package:kuaishai2/features/programs/models/step.dart';
void main() {
group('DeviceMessageType', () {
test('wire names match protocol', () {
expect(DeviceMessageType.deviceInfo.wireName, 'device_info');
expect(DeviceMessageType.createTask.wireName, 'create_task');
expect(DeviceMessageType.lightControl.wireName, 'light_control');
expect(DeviceMessageType.control.wireName, 'control');
});
test('fromWire round-trip', () {
for (final t in DeviceMessageType.values) {
expect(DeviceMessageType.fromWire(t.wireName), t);
}
expect(DeviceMessageType.fromWire(null), isNull);
expect(DeviceMessageType.fromWire('nope'), isNull);
});
});
group('DeviceMessage', () {
test('toJson 与数据模型一致', () {
final msg = DeviceMessage(
messageId: 'abc-1',
type: DeviceMessageType.deviceInfo,
data: const {
'door_status': 'open',
'task_status': 'running',
'light_status': 'on',
},
);
final json = msg.toJson();
expect(json['message_id'], 'abc-1');
expect(json['type'], 'device_info');
expect(json['ack'], isNull);
expect(json['need_ack'], isFalse);
expect((json['data'] as Map)['door_status'], 'open');
});
test('encode/decode 闭环', () {
final src = DeviceMessage(
messageId: 'm-001',
type: DeviceMessageType.lightControl,
data: const {'status': 'on'},
ack: 'reply-001',
needAck: true,
);
final raw = src.encode();
final parsed = DeviceMessage.decode(raw);
expect(parsed, isNotNull);
expect(parsed!.messageId, 'm-001');
expect(parsed.type, DeviceMessageType.lightControl);
expect(parsed.ack, 'reply-001');
expect(parsed.needAck, isTrue);
expect(parsed.data['status'], 'on');
});
test('decode 非法 JSON 返回 null', () {
expect(DeviceMessage.decode('not-json'), isNull);
expect(DeviceMessage.decode('{"foo":1}'), isNull);
expect(DeviceMessage.decode(jsonEncode({
'message_id': '',
'type': 'device_info',
'data': {},
})), isNull);
});
test('ackFor 工厂生成应答消息', () {
final original = DeviceMessage(
messageId: 'orig-1',
type: DeviceMessageType.createTask,
data: const {'k': 'v'},
);
final ack = DeviceMessage.ackFor(original, {'result': 'ok'});
expect(ack.ack, 'orig-1');
expect(ack.type, DeviceMessageType.createTask);
expect(ack.data['result'], 'ok');
});
});
group('MessageIdGenerator', () {
test('连续生成的 ID 互不相同', () {
final gen = MessageIdGenerator();
final ids = List.generate(100, (_) => gen.next()).toSet();
expect(ids.length, 100);
});
});
group('JsonProtocolService', () {
test('encode + tryDecode 闭环', () {
final p = JsonProtocolService();
final msg = DeviceMessage(
messageId: 'p-1',
type: DeviceMessageType.control,
data: const {'status': 'stop'},
);
final bytes = p.encode(msg);
// 4 字节长度前缀 + 至少 1 字节 JSON + 1 字节换行
expect(bytes.length, greaterThan(5));
final (decoded, consumed) = p.tryDecode(bytes);
expect(consumed, bytes.length);
expect(decoded, isNotNull);
expect(decoded!.type, DeviceMessageType.control);
expect(decoded.data['status'], 'stop');
});
test('不足一帧时返回 null', () {
final p = JsonProtocolService();
final partial = Uint8List.fromList([0x00, 0x00, 0x00]); // 只有 3 字节长度前缀
final (decoded, consumed) = p.tryDecode(partial);
expect(decoded, isNull);
expect(consumed, 0);
});
test('分片到达也能解析', () {
final p = JsonProtocolService();
final msg = DeviceMessage(
messageId: 'frag-1',
type: DeviceMessageType.createTask,
data: const {'x': 1},
);
final bytes = p.encode(msg);
// 拆成 1 字节 / 剩余两段
final first = bytes.sublist(0, 1);
final second = bytes.sublist(1, 4);
final third = bytes.sublist(4);
expect(p.tryDecode(first).$1, isNull);
final (m1, _) = p.tryDecode(second);
expect(m1, isNull);
final (m2, consumed) = p.tryDecode(third);
expect(m2, isNotNull);
expect(m2!.messageId, 'frag-1');
// consumed = 4 字节长度前缀 + json 长度 + 1 字节换行
expect(consumed, bytes.length);
});
test('长度字段异常时丢弃首字节重新对齐', () {
final p = JsonProtocolService();
// 长度 = 0xFFFFFF 不可能
final corrupted = Uint8List.fromList([0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x01, 0x7B, 0x7D, 0x0A]);
final (decoded, _) = p.tryDecode(corrupted);
// 长度异常 -> 丢弃 1 字节 + 下一帧有效
// 第二帧 length=1, json='{', 缺少换行也允许
expect(decoded, isNull);
});
});
group('TaskPayload → DeviceMessage', () {
test('构造 create_task 消息字段与文档一致', () {
final program = Program(
id: 1,
code: 'P001',
name: 'Demo',
createdAt: '2026-06-04',
temperature: 50,
airflowTime: 60,
);
final steps = [
Step(
id: 1,
programId: 1,
stepNo: 1,
position: 'A1',
name: '混合',
mixTime: 60,
magnetTime: 30,
volume: 100,
speed: 5,
),
];
final payload = TaskPayload.fromProgram(program, steps);
final msg = payload.toMessage('mid-1', needAck: true);
expect(msg.type, DeviceMessageType.createTask);
expect(msg.needAck, isTrue);
expect(msg.data['temperature'], 50);
expect(msg.data['airflowtime'], 60);
final stepJson = (msg.data['steps'] as List).first as Map<String, dynamic>;
expect(stepJson['no'], 1);
expect(stepJson['slot'], 1); // A1 -> 1
expect(stepJson['mixtime'], 60);
expect(stepJson['pulltime'], 30);
expect(stepJson['volume'], 100);
expect(stepJson['speed'], 5);
});
});
group('DeviceState model', () {
test('copyWith 不会修改未传字段', () {
const s = DeviceState(
lightStatus: DeviceLightStatus.on,
doorStatus: DeviceDoorStatus.open,
taskStatus: DeviceTaskStatus.running,
lastInfoAt: null,
);
final s2 = s.copyWith(lightStatus: DeviceLightStatus.off);
expect(s2.lightStatus, DeviceLightStatus.off);
expect(s2.doorStatus, DeviceDoorStatus.open);
expect(s2.taskStatus, DeviceTaskStatus.running);
});
test('infoStale 在 10 秒后为 true', () {
final past = DateTime.now().subtract(const Duration(seconds: 30));
final s = DeviceState(lastInfoAt: past);
expect(s.infoStale, isTrue);
});
});
group('DeviceLog', () {
test('summarizeData 截断长字符串与多余键', () {
final data = {
'a': 1,
'b': 'short',
'c': 'x' * 200,
'd': [1, 2, 3],
'e': true,
'f': null,
'g': 'extra',
'h': 'extra2',
'i': 'extra3',
};
final summary = DeviceLog.summarizeData(data, maxKeys: 4);
expect(summary.contains('a=1'), isTrue);
expect(summary.contains('b="short"'), isTrue);
expect(summary.contains(''), isTrue); // 长字符串被截断
expect(summary.contains(''), isTrue);
expect(summary.contains(', ...'), isTrue); // 多余键被截断
});
test('summarizeData 空 Map / null 友好处理', () {
expect(DeviceLog.summarizeData(null), '{}');
expect(DeviceLog.summarizeData({}), '{}');
});
test('info / warn / severe 不抛异常', () {
// 仅验证调用不抛异常dart:developer.log 在测试环境无可见输出
DeviceLog.info('hello');
DeviceLog.warn('warn', error: StateError('x'));
DeviceLog.severe('severe', error: 'err');
});
});
}