Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions lib/ble/ble_engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,22 @@ class BleEngine {
hasLiveConsumer: _liveEnabled && !_liveHrOnly,
);

/// The last hop: the policy's [LinkPriority] as the radio's own enum.
///
/// Lifted out of [_applyLinkPriority] because inline it was the one step
/// nothing covered. `desiredLinkPriority` could keep returning exactly the
/// right answer while every arm here mapped to `ConnectionPriority.high` —
/// which IS issue #200, the link pinned at ~11.25 ms overnight — and the
/// whole suite stayed green. Arm-by-arm coverage is in
/// `link_priority_policy_test.dart`.
@visibleForTesting
static ConnectionPriority connectionPriorityFor(LinkPriority want) =>
switch (want) {
LinkPriority.high => ConnectionPriority.high,
LinkPriority.balanced => ConnectionPriority.balanced,
LinkPriority.lowPower => ConnectionPriority.lowPower,
};

@visibleForTesting
void debugBeginConnectSetup() => _connectSetup = true;

Expand Down Expand Up @@ -685,11 +701,7 @@ class BleEngine {
final generation = _linkGeneration;
try {
await session.device.requestConnectionPriority(
connectionPriorityRequest: switch (want) {
LinkPriority.high => ConnectionPriority.high,
LinkPriority.balanced => ConnectionPriority.balanced,
LinkPriority.lowPower => ConnectionPriority.lowPower,
},
connectionPriorityRequest: connectionPriorityFor(want),
);
// Only remember it if the link we asked is still the live one. A
// teardown during the await clears `_appliedPriority` precisely so
Expand Down
105 changes: 105 additions & 0 deletions test/dart_source_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Regression cases for the scanner the structural tests are built on.
//
// Every case here is a way the previous per-line regex helper got it wrong.
// They matter because when this primitive fails, it fails invisibly: the grep
// test it feeds goes green either way.

import 'package:flutter_test/flutter_test.dart';

import 'support/dart_source.dart';

void main() {
test('line count and column positions survive', () {
const src = "a();\n// gone\nb('str');\n";
final lines = codeLines(src);
expect(lines, hasLength(4));
expect(lines[0], 'a();');
expect(lines[1].trim(), isEmpty);
// The literal goes, quotes included, but its width is kept so columns
// still line up with the original source.
expect(lines[2], 'b( );');
expect(lines[2], hasLength("b('str');".length));
});

test("a '//' inside a string does not truncate the rest of the line", () {
// The bug: stripping `//` before strings ate everything after the URL,
// including a real call the guard exists to catch.
const src = "const u = 'https://x/y'; d.requestConnectionPriority(1);";
final code = stripCommentsAndStrings(src);
expect(code, contains('requestConnectionPriority'));
expect(code, isNot(contains('https')));
});

test('braces inside a string are removed, not counted', () {
// lib/ui/kit/route_map.dart is a live instance of this.
const src = "const t = 'https://{s}.tiles/{z}/{x}/{y}{r}.png';";
final code = stripCommentsAndStrings(src);
expect(code.contains('{'), isFalse);
expect(code.contains('}'), isFalse);
});

test('block comments are stripped, and they nest', () {
const src = 'a(); /* x /* y */ z */ b();';
final code = stripCommentsAndStrings(src);
expect(code, contains('a();'));
expect(code, contains('b();'));
expect(code, isNot(contains('x')));
expect(code, isNot(contains('z')));
});

test('a multi-line block comment keeps its newlines', () {
const src = 'a();\n/* one\n two\n */\nb();';
final lines = codeLines(src);
expect(lines, hasLength(5));
expect(lines[4], 'b();');
expect(lines[1].trim(), isEmpty);
expect(lines[2].trim(), isEmpty);
});

test('a token named in a block comment is not a match', () {
const src =
'/* used to call d.requestConnectionPriority(x) here */\nok();';
expect(
stripCommentsAndStrings(src),
isNot(contains('requestConnectionPriority')),
);
});

test('triple-quoted strings are stripped across lines', () {
// No per-line regex can do this, and lib/ has ~80 such lines.
const src = "final q = '''\nrequestConnectionPriority(\n{{{\n''';\nok();";
final code = stripCommentsAndStrings(src);
expect(code, isNot(contains('requestConnectionPriority')));
expect(code.contains('{'), isFalse);
expect(code, contains('ok();'));
expect(codeLines(src), hasLength(5));
});

test('interpolation containing a quote does not end the string early', () {
const src = "_log('a \${m['k']} b'); real();";
final code = stripCommentsAndStrings(src);
expect(code, contains('_log('));
expect(code, contains('real();'));
expect(code, isNot(contains('k')));
expect(code.contains('{'), isFalse);
});

test('a string inside interpolation can itself hold a comment marker', () {
const src = "final s = '\${f('//')} tail'; kept();";
expect(stripCommentsAndStrings(src), contains('kept();'));
});

test('escapes do not terminate a string, and raw strings ignore them', () {
expect(stripCommentsAndStrings(r"var a = 'x\'y'; z();"), contains('z();'));
expect(stripCommentsAndStrings(r"var a = r'x\'; z();"), contains('z();'));
});

test('the engine failure log is not counted as a call site', () {
// The concrete reason the structural test needs string stripping at all.
const src = r"_log('requestConnectionPriority(${want.name}) failed: $e');";
expect(
stripCommentsAndStrings(src),
isNot(contains('requestConnectionPriority')),
);
});
}
51 changes: 51 additions & 0 deletions test/link_priority_policy_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
// offload always runs at the fast interval, whatever else is going on, because
// throughput during a drain is what the fast interval was for.

import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:openstrap_edge/ble/ble_engine.dart';
import 'package:openstrap_edge/sync/sync_policy.dart';
Expand Down Expand Up @@ -84,6 +85,56 @@ void main() {
}
});

group('the policy survives the last hop to the radio', () {
// The gap this closes: `desiredLinkPriority` can keep returning exactly
// the right LinkPriority while the switch that turns it into the radio's
// own enum maps every arm to `ConnectionPriority.high`. That IS issue #200
// — the link pinned at ~11.25 ms overnight — and with the mapping inline
// and untested, the whole suite stayed green through it. Verified by
// mutation: all three arms to `.high` passed 13/13 before this existed.
const expected = {
LinkPriority.high: ConnectionPriority.high,
LinkPriority.balanced: ConnectionPriority.balanced,
LinkPriority.lowPower: ConnectionPriority.lowPower,
};

test('every arm maps to its own priority, none of them to high', () {
for (final want in LinkPriority.values) {
expect(
BleEngine.connectionPriorityFor(want),
expected[want],
reason: '${want.name} must not be silently promoted',
);
}
});

test('the mapping is exhaustive, and no two arms collapse', () {
// A LinkPriority added to the enum has to be given a mapping here rather
// than inheriting whatever the switch falls through to.
expect(expected.keys, unorderedEquals(LinkPriority.values));
expect(
LinkPriority.values.map(BleEngine.connectionPriorityFor).toSet(),
hasLength(LinkPriority.values.length),
reason: 'two link priorities collapsing to one radio priority means '
'one of the steps is not actually a step',
);
});

test('the overnight state reaches the radio as lowPower', () {
// End to end through both halves: the rule, then the mapping.
expect(
BleEngine.connectionPriorityFor(
desiredLinkPriority(
offloadActive: false,
background: true,
hasLiveConsumer: false,
),
),
ConnectionPriority.lowPower,
);
});
});

test('the battery poll is minutes apart, not seconds', () {
// It rode the 30 s keep-alive tick: 2,880 radio round-trips a day for a
// display value that changes a handful of times.
Expand Down
Loading
Loading