You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
meshcore-client/test/device_population_test.dart

337 lines
13 KiB

import 'dart:convert';
import 'dart:typed_data';
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_open/connector/device_population.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/storage/drift/blob_store.dart';
import 'package:meshcore_open/storage/drift/offband_database.dart';
import 'package:meshcore_open/storage/prefs_manager.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Builds a device-info reply frame of [length] bytes with [verCode] at
/// byte 1, mirroring RESP_CODE_DEVICE_INFO layouts:
/// stock ships exactly 82 bytes; the Offband fork appends offband_caps at
/// offset 82 (83 bytes) and caps byte 2 at offset 84 (85 bytes).
Uint8List deviceInfoFrame({required int length, required int verCode}) {
final frame = Uint8List(length);
if (length > 0) frame[0] = 13; // RESP_CODE_DEVICE_INFO
if (length > 1) frame[1] = verCode;
return frame;
}
void main() {
group('classifyDevicePopulation', () {
test('stock 1.17.x: 82-byte frame, ver 13', () {
final c = classifyDevicePopulation(
deviceInfo: deviceInfoFrame(length: 82, verCode: 13),
firmwareVersion: 'v1.17.0',
);
expect(c.population, DevicePopulation.stockLegacy);
expect(c.allowsOffbandFrames, isFalse);
expect(c.allowsCliFrame, isFalse);
});
test('stock 1.18.0: 82-byte frame, ver 14', () {
final c = classifyDevicePopulation(
deviceInfo: deviceInfoFrame(length: 82, verCode: 14),
firmwareVersion: 'v1.18.0',
);
expect(c.population, DevicePopulation.stockModern);
expect(c.allowsOffbandFrames, isFalse);
expect(c.allowsCliFrame, isTrue);
});
test('Offband ver 22: 83-byte frame (caps byte 1 only)', () {
final c = classifyDevicePopulation(
deviceInfo: deviceInfoFrame(length: 83, verCode: 22),
firmwareVersion: '1.4.0-1.17.0',
);
expect(c.population, DevicePopulation.offband);
expect(c.allowsOffbandFrames, isTrue);
// No frame-66 CLI on the 1.17.0-base fork; flips only post-absorb.
expect(c.allowsCliFrame, isFalse);
});
test('Offband ver 22: 85-byte frame (caps byte 2 present)', () {
final c = classifyDevicePopulation(
deviceInfo: deviceInfoFrame(length: 85, verCode: 22),
firmwareVersion: '1.4.0-1.17.0',
);
expect(c.population, DevicePopulation.offband);
expect(c.allowsOffbandFrames, isTrue);
});
test(
'Offband release image X-X version string (#691) still classifies',
() {
final c = classifyDevicePopulation(
deviceInfo: deviceInfoFrame(length: 85, verCode: 22),
firmwareVersion: '1.4.0-1.4.0',
);
expect(c.population, DevicePopulation.offband);
},
);
test('ver >= 14 with a LONG frame but stock version string is a conflict:'
' fails closed to stockLegacy', () {
final c = classifyDevicePopulation(
deviceInfo: deviceInfoFrame(length: 83, verCode: 14),
firmwareVersion: 'v1.18.0',
);
expect(c.population, DevicePopulation.stockLegacy);
expect(c.allowsOffbandFrames, isFalse);
expect(c.allowsCliFrame, isFalse);
expect(c.conflict, isTrue);
});
test('malformed short frame fails closed', () {
final c = classifyDevicePopulation(
deviceInfo: deviceInfoFrame(length: 3, verCode: 0),
firmwareVersion: null,
);
expect(c.population, DevicePopulation.stockLegacy);
expect(c.allowsOffbandFrames, isFalse);
expect(c.allowsCliFrame, isFalse);
});
test('fork-shape frame with NO readable version fails closed'
' (uncorroborated shape never unlocks fork frames)', () {
final c = classifyDevicePopulation(
deviceInfo: deviceInfoFrame(length: 83, verCode: 22),
firmwareVersion: null,
);
expect(c.population, DevicePopulation.stockLegacy);
expect(c.conflict, isTrue);
expect(c.allowsOffbandFrames, isFalse);
});
test('fork-shape frame with an unrecognizable version fails closed', () {
final c = classifyDevicePopulation(
deviceInfo: deviceInfoFrame(length: 83, verCode: 22),
firmwareVersion: 'my-custom-build',
);
// #878: 'my-custom-build' is no semver, so it corroborates nothing -
// the fork shape stays uncorroborated and the device fails closed.
expect(c.population, DevicePopulation.stockLegacy);
expect(c.conflict, isTrue);
});
group('classifyVersionString (#878)', () {
test('stock forms', () {
expect(classifyVersionString('v1.18.0'), VersionStringShape.stock);
expect(classifyVersionString('1.18.0'), VersionStringShape.stock);
expect(classifyVersionString(' v1.17.1 '), VersionStringShape.stock);
});
test('Offband dual forms', () {
expect(
classifyVersionString('1.4.0-1.17.0'),
VersionStringShape.offbandDual,
);
expect(
classifyVersionString('1.4.0-1.4.0'), // release image X-X (#691)
VersionStringShape.offbandDual,
);
expect(
classifyVersionString('1.1.0-4-g9f2e1c3-dirty'), // bench build
VersionStringShape.offbandDual,
);
});
test('unknown forms', () {
expect(classifyVersionString(null), VersionStringShape.unknown);
expect(classifyVersionString(''), VersionStringShape.unknown);
expect(
classifyVersionString('my-custom-build'),
VersionStringShape.unknown,
);
expect(classifyVersionString('v1.18'), VersionStringShape.unknown);
// A stock pre-release / git-suffixed string reads as dual at the parser
// level; that is harmless now, because a stock-shape frame is classified
// by ver code regardless of the version string (#946).
expect(
classifyVersionString('v1.19.0-rc1'),
VersionStringShape.offbandDual,
);
expect(classifyVersionString('-1.17.0'), VersionStringShape.unknown);
});
});
test('real stock 1.18.0 with a git-suffixed version classifies'
' stockModern (#946, hardware)', () {
// The field device reports "v1.18.0-81dbae5" (semver + git-describe
// hash), which classifyVersionString reads as the dual form. An 82-byte
// frame is definitively stock (Offband always appends caps bytes), so the
// version string must not override the shape: the console must be offered.
final c = classifyDevicePopulation(
deviceInfo: deviceInfoFrame(length: 82, verCode: 14),
firmwareVersion: 'v1.18.0-81dbae5',
);
expect(c.population, DevicePopulation.stockModern);
expect(c.allowsCliFrame, isTrue);
expect(c.conflict, isFalse);
});
test('stock-shape frame with a dual-looking version is still stockModern:'
' the 82-byte shape is definitive (#946)', () {
// Previously an inverse conflict (fail closed); the shape is definitive,
// so a stock-shape frame is never forced to a conflict by its version.
final c = classifyDevicePopulation(
deviceInfo: deviceInfoFrame(length: 82, verCode: 14),
firmwareVersion: '1.4.0-1.17.0',
);
expect(c.population, DevicePopulation.stockModern);
expect(c.conflict, isFalse);
expect(c.allowsCliFrame, isTrue);
});
test('length 84 with a dual version string classifies offband', () {
final c = classifyDevicePopulation(
deviceInfo: deviceInfoFrame(length: 84, verCode: 22),
firmwareVersion: '1.4.0-1.17.0',
);
expect(c.population, DevicePopulation.offband);
});
test('ver at maxKnownVerCode with stock shape and stock string stays'
' stockModern; one above fails closed', () {
final at = classifyDevicePopulation(
deviceInfo: deviceInfoFrame(length: 82, verCode: 32),
firmwareVersion: 'v9.9.9',
);
expect(at.population, DevicePopulation.stockModern);
final above = classifyDevicePopulation(
deviceInfo: deviceInfoFrame(length: 82, verCode: 33),
firmwareVersion: 'v9.9.9',
);
expect(above.population, DevicePopulation.stockLegacy);
});
test('stock ver wildly out of range fails closed', () {
final c = classifyDevicePopulation(
deviceInfo: deviceInfoFrame(length: 82, verCode: 255),
firmwareVersion: 'v9.9.9',
);
expect(c.population, DevicePopulation.stockLegacy);
});
});
group('connector routes caps parsing through the classifier (#876)', () {
late OffbandDatabase db;
late MeshCoreConnector connector;
/// Full RESP_CODE_DEVICE_INFO frame with the version string at its real
/// offset (60, 20 bytes NUL-padded) and optional fork tail bytes.
Uint8List deviceInfo({
required int verCode,
required String version,
int length = 82,
int caps = 0x21,
int caps2 = 0x0F,
}) {
final frame = Uint8List(length);
frame[0] = 13; // RESP_CODE_DEVICE_INFO
frame[1] = verCode;
final v = ascii.encode(version);
frame.setRange(60, 60 + v.length, v);
if (length > 82) frame[82] = caps;
if (length > 84) frame[84] = caps2;
return frame;
}
setUp(() async {
SharedPreferences.setMockInitialValues({});
PrefsManager.reset();
await PrefsManager.initialize();
db = OffbandDatabase(NativeDatabase.memory());
BlobStore.overrideForTest(BlobStore(db));
connector = MeshCoreConnector();
connector.sendFrameOverrideForTest = (_) {};
connector.setConnectedForTest();
});
tearDown(() async {
BlobStore.clearTestOverride();
await db.close();
});
test('Offband frame with dual version populates caps', () {
connector.handleFrameForTest(
deviceInfo(verCode: 22, version: '1.4.0-1.17.0', length: 85),
);
expect(connector.classification?.population, DevicePopulation.offband);
expect(connector.offbandCaps, 0x21);
expect(connector.offbandCaps2, 0x0F);
// femLna byte at offset 83 defaults to 0 in the fixture = bypass.
expect(connector.femLnaEnabled, isFalse);
});
test('83-byte Offband frame: caps byte 1 present, byte 2 and FEM state'
' absent', () {
connector.handleFrameForTest(
deviceInfo(verCode: 22, version: '1.4.0-1.17.0', length: 83),
);
expect(connector.classification?.population, DevicePopulation.offband);
expect(connector.offbandCaps, 0x21);
expect(connector.offbandCaps2, isNull);
expect(connector.femLnaEnabled, isNull);
});
test('fork-length frame with a stock version string leaves every Offband'
' field null', () {
connector.handleFrameForTest(
deviceInfo(verCode: 14, version: 'v1.18.0', length: 85),
);
expect(
connector.classification?.population,
DevicePopulation.stockLegacy,
);
expect(connector.classification?.conflict, isTrue);
expect(connector.offbandCaps, isNull);
expect(connector.offbandCaps2, isNull);
});
test('stock 1.18 frame classifies stockModern with null caps', () {
connector.handleFrameForTest(deviceInfo(verCode: 14, version: 'v1.18.0'));
expect(
connector.classification?.population,
DevicePopulation.stockModern,
);
expect(connector.offbandCaps, isNull);
});
test('fork gates never open on a stock device, whatever its ver code'
' (#877): stock reuses the fork range 14+ with other meanings', () {
// A hypothetical future stock device reporting ver 22 (the fork's
// current contract number) must not light any Offband capability.
connector.handleFrameForTest(deviceInfo(verCode: 22, version: 'v9.9.9'));
expect(connector.supportsOffbandGps, isFalse);
expect(connector.supportsOffbandBlock, isFalse);
expect(connector.supportsOffbandFemLna, isFalse);
expect(connector.supportsButtonMatrix, isFalse);
expect(connector.supportsNotifyScope, isFalse);
expect(connector.supportsPktHash, isFalse);
// Pre-split floors (same meaning on every population) stay available.
expect(connector.supportsCompanionRadioStats, isTrue);
});
test('stock at ver 14 lights nothing either (#1065): the real stock code,'
' now that the fork rolls back onto the same number', () {
// The case above uses 22, which no stock device reports. 14 is what real
// stock 1.18 sends AND what the fork reports after the rollback, so this
// is the collision in its actual form. The three gates whose version
// floors were removed in #1065 must still be false here, and they are,
// because the 82-byte shape classifies stockModern and the connector
// nulls both caps bytes.
connector.handleFrameForTest(
deviceInfo(verCode: 14, version: 'v1.18.0-81dbae5'),
);
expect(connector.offbandCaps, isNull);
expect(connector.offbandCaps2, isNull);
expect(connector.supportsOffbandBlock, isFalse);
expect(connector.supportsOffbandCaplog, isFalse);
expect(connector.supportsPktHash, isFalse);
});
});
}

Powered by TurnKey Linux.