flutter/packages/flutter_tools/test/general.shard/mdns_discovery_test.dart
Victoria Ashworth e841350bfc
Don't throw on error for mDNS when searching for Dart VML url on core devices (#167135)
When we discover the Dart VM for iOS Core Devices (iOS 17+), we use both
the device logs and mDNS simultaneously. Since mDNS is having issues
with macOS 15, don't throw if mDNS errors - this will allow the Dart VM
to have a chance to be found by the device logs instead.

Improving experience for
https://github.com/flutter/flutter/issues/150131.

Also should unblock https://github.com/flutter/flutter/issues/166843.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-04-16 22:46:13 +00:00

1296 lines
46 KiB
Dart

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/device_port_forwarder.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/mdns_discovery.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:multicast_dns/multicast_dns.dart';
import 'package:test/fake.dart';
import 'package:unified_analytics/unified_analytics.dart';
import '../src/common.dart';
import '../src/fakes.dart';
void main() {
group('mDNS Discovery', () {
final int future = DateTime.now().add(const Duration(days: 1)).millisecondsSinceEpoch;
setUp(() {
setNetworkInterfaceLister(
({bool? includeLoopback, bool? includeLinkLocal, InternetAddressType? type}) async =>
<NetworkInterface>[],
);
});
tearDown(() {
resetNetworkInterfaceLister();
});
group('for attach', () {
late MDnsClient emptyClient;
setUp(() {
emptyClient = FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{});
});
testWithoutContext('Find result in preliminary client', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[PtrResourceRecord('foo', future, domainName: 'bar')],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
],
},
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: emptyClient,
preliminaryMDnsClient: client,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
final MDnsVmServiceDiscoveryResult? result = await portDiscovery.queryForAttach();
expect(result, isNotNull);
});
testWithoutContext(
'Do not find result in preliminary client, but find in main client',
() async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[PtrResourceRecord('foo', future, domainName: 'bar')],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord(
'bar',
future,
port: 123,
weight: 1,
priority: 1,
target: 'appId',
),
],
},
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
preliminaryMDnsClient: emptyClient,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
final MDnsVmServiceDiscoveryResult? result = await portDiscovery.queryForAttach();
expect(result, isNotNull);
},
);
testWithoutContext('Find multiple in preliminary client', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', future, domainName: 'bar'),
PtrResourceRecord('baz', future, domainName: 'fiz'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
],
'fiz': <SrvResourceRecord>[
SrvResourceRecord('fiz', future, port: 321, weight: 1, priority: 1, target: 'local'),
],
},
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: emptyClient,
preliminaryMDnsClient: client,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
expect(portDiscovery.queryForAttach, throwsToolExit());
});
testWithoutContext('Find duplicates in preliminary client', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', future, domainName: 'bar'),
PtrResourceRecord('foo', future, domainName: 'bar'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
],
},
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: emptyClient,
preliminaryMDnsClient: client,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
final MDnsVmServiceDiscoveryResult? result = await portDiscovery.queryForAttach();
expect(result, isNotNull);
});
testWithoutContext('Find similar named in preliminary client', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', future, domainName: 'bar'),
PtrResourceRecord('foo', future, domainName: 'bar (2)'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
],
'bar (2)': <SrvResourceRecord>[
SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
],
},
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: emptyClient,
preliminaryMDnsClient: client,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
expect(portDiscovery.queryForAttach, throwsToolExit());
});
testWithoutContext('No ports available', () async {
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: emptyClient,
preliminaryMDnsClient: emptyClient,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
final int? port = (await portDiscovery.queryForAttach())?.port;
expect(port, isNull);
});
testWithoutContext(
'Prints helpful message when there is no ipv4 link local address.',
() async {
final BufferLogger logger = BufferLogger.test();
final MemoryFileSystem fs = MemoryFileSystem.test();
final FakeAnalytics fakeAnalytics = getInitializedFakeAnalyticsInstance(
fs: fs,
fakeFlutterVersion: FakeFlutterVersion(),
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: emptyClient,
preliminaryMDnsClient: emptyClient,
logger: logger,
analytics: fakeAnalytics,
);
final Uri? uri = await portDiscovery.getVMServiceUriForAttach('', FakeIOSDevice());
expect(uri, isNull);
expect(logger.errorText, contains('Personal Hotspot'));
expect(
fakeAnalytics.sentEvents,
contains(Event.appleUsageEvent(workflow: 'ios-mdns', parameter: 'no-ipv4-link-local')),
);
},
);
testWithoutContext('One port available, no appId', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[PtrResourceRecord('foo', future, domainName: 'bar')],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
],
},
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
preliminaryMDnsClient: emptyClient,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
final int? port = (await portDiscovery.queryForAttach())?.port;
expect(port, 123);
});
testWithoutContext('One port available, no appId, with authCode', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[PtrResourceRecord('foo', future, domainName: 'bar')],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
],
},
txtResponse: <String, List<TxtResourceRecord>>{
'bar': <TxtResourceRecord>[TxtResourceRecord('bar', future, text: 'authCode=xyz\n')],
},
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
preliminaryMDnsClient: emptyClient,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
final MDnsVmServiceDiscoveryResult? result = await portDiscovery.queryForAttach();
expect(result?.port, 123);
expect(result?.authCode, 'xyz/');
});
testWithoutContext('Multiple ports available, with appId', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', future, domainName: 'bar'),
PtrResourceRecord('baz', future, domainName: 'fiz'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
],
'fiz': <SrvResourceRecord>[
SrvResourceRecord('fiz', future, port: 321, weight: 1, priority: 1, target: 'local'),
],
},
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
preliminaryMDnsClient: emptyClient,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
final int? port = (await portDiscovery.queryForAttach(applicationId: 'fiz'))?.port;
expect(port, 321);
});
testWithoutContext('Multiple ports available per process, with appId', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', future, domainName: 'bar'),
PtrResourceRecord('baz', future, domainName: 'fiz'),
],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'),
SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
],
'fiz': <SrvResourceRecord>[
SrvResourceRecord('fiz', future, port: 4321, weight: 1, priority: 1, target: 'local'),
SrvResourceRecord('fiz', future, port: 321, weight: 1, priority: 1, target: 'local'),
],
},
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
preliminaryMDnsClient: emptyClient,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
final int? port = (await portDiscovery.queryForAttach(applicationId: 'bar'))?.port;
expect(port, 1234);
});
testWithoutContext('Throws Exception when client throws OSError on start', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[],
<String, List<SrvResourceRecord>>{},
osErrorOnStart: true,
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
preliminaryMDnsClient: emptyClient,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
expect(() async => portDiscovery.queryForAttach(), throwsException);
});
testWithoutContext('Correctly builds VM Service URI with hostVmservicePort == 0', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[PtrResourceRecord('foo', future, domainName: 'bar')],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
],
},
);
final FakeIOSDevice device = FakeIOSDevice();
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
preliminaryMDnsClient: emptyClient,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
final Uri? uri = await portDiscovery.getVMServiceUriForAttach(
'bar',
device,
hostVmservicePort: 0,
);
expect(uri.toString(), 'http://127.0.0.1:123/');
});
testWithoutContext('Get wireless device IP (iPv4)', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[PtrResourceRecord('foo', future, domainName: 'bar')],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'),
],
},
ipResponse: <String, List<IPAddressResourceRecord>>{
'appId': <IPAddressResourceRecord>[
IPAddressResourceRecord(
'Device IP',
0,
address: InternetAddress.tryParse('111.111.111.111')!,
),
],
},
txtResponse: <String, List<TxtResourceRecord>>{
'bar': <TxtResourceRecord>[TxtResourceRecord('bar', future, text: 'authCode=xyz\n')],
},
);
final FakeIOSDevice device = FakeIOSDevice();
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
preliminaryMDnsClient: emptyClient,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
final Uri? uri = await portDiscovery.getVMServiceUriForAttach(
'bar',
device,
useDeviceIPAsHost: true,
);
expect(uri.toString(), 'http://111.111.111.111:1234/xyz/');
});
testWithoutContext('Get wireless device IP (iPv6)', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[PtrResourceRecord('foo', future, domainName: 'bar')],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'),
],
},
ipResponse: <String, List<IPAddressResourceRecord>>{
'appId': <IPAddressResourceRecord>[
IPAddressResourceRecord(
'Device IP',
0,
address: InternetAddress.tryParse('1111:1111:1111:1111:1111:1111:1111:1111')!,
),
],
},
txtResponse: <String, List<TxtResourceRecord>>{
'bar': <TxtResourceRecord>[TxtResourceRecord('bar', future, text: 'authCode=xyz\n')],
},
);
final FakeIOSDevice device = FakeIOSDevice();
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
preliminaryMDnsClient: emptyClient,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
final Uri? uri = await portDiscovery.getVMServiceUriForAttach(
'bar',
device,
useDeviceIPAsHost: true,
);
expect(uri.toString(), 'http://[1111:1111:1111:1111:1111:1111:1111:1111]:1234/xyz/');
});
testWithoutContext(
'Throw error if unable to find VM service with app id and device port',
() async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', future, domainName: 'srv-foo'),
PtrResourceRecord('bar', future, domainName: 'srv-bar'),
PtrResourceRecord('baz', future, domainName: 'srv-boo'),
],
<String, List<SrvResourceRecord>>{
'srv-foo': <SrvResourceRecord>[
SrvResourceRecord(
'srv-foo',
future,
port: 123,
weight: 1,
priority: 1,
target: 'target-foo',
),
],
'srv-bar': <SrvResourceRecord>[
SrvResourceRecord(
'srv-bar',
future,
port: 123,
weight: 1,
priority: 1,
target: 'target-bar',
),
],
'srv-baz': <SrvResourceRecord>[
SrvResourceRecord(
'srv-baz',
future,
port: 123,
weight: 1,
priority: 1,
target: 'target-baz',
),
],
},
);
final FakeIOSDevice device = FakeIOSDevice();
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
preliminaryMDnsClient: emptyClient,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
expect(
portDiscovery.getVMServiceUriForAttach('srv-bar', device, deviceVmservicePort: 321),
throwsToolExit(
message: 'Did not find a Dart VM Service advertised for srv-bar on port 321.',
),
);
},
);
testWithoutContext('Throw error if unable to find VM Service with app id', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[PtrResourceRecord('foo', future, domainName: 'srv-foo')],
<String, List<SrvResourceRecord>>{
'srv-foo': <SrvResourceRecord>[
SrvResourceRecord(
'srv-foo',
future,
port: 123,
weight: 1,
priority: 1,
target: 'target-foo',
),
],
},
);
final FakeIOSDevice device = FakeIOSDevice();
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
preliminaryMDnsClient: emptyClient,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
expect(
portDiscovery.getVMServiceUriForAttach('srv-asdf', device),
throwsToolExit(message: 'Did not find a Dart VM Service advertised for srv-asdf.'),
);
});
});
group('for launch', () {
testWithoutContext('Ensure either port or device name are provided', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[],
<String, List<SrvResourceRecord>>{},
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
expect(
() async => portDiscovery.queryForLaunch(applicationId: 'app-id'),
throwsAssertionError,
);
});
testWithoutContext('No ports available', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[],
<String, List<SrvResourceRecord>>{},
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
final MDnsVmServiceDiscoveryResult? result = await portDiscovery.queryForLaunch(
applicationId: 'app-id',
deviceVmservicePort: 123,
);
expect(result, null);
});
testWithoutContext(
'Prints helpful message when there is no ipv4 link local address.',
() async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[],
<String, List<SrvResourceRecord>>{},
);
final BufferLogger logger = BufferLogger.test();
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: logger,
analytics: const NoOpAnalytics(),
);
final Uri? uri = await portDiscovery.getVMServiceUriForLaunch(
'',
FakeIOSDevice(),
deviceVmservicePort: 0,
);
expect(uri, isNull);
expect(logger.errorText, contains('Personal Hotspot'));
},
);
testWithoutContext('Throws Exception when client throws OSError on start', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[],
<String, List<SrvResourceRecord>>{},
osErrorOnStart: true,
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
expect(
() async =>
portDiscovery.queryForLaunch(applicationId: 'app-id', deviceVmservicePort: 123),
throwsException,
);
});
// On macOS, the mDNS client's socket stream creates a SocketException if
// the app running the tool does not have Local Network permissions.
// See: https://github.com/flutter/flutter/issues/150131
test(
'On macOS, tool exits with a helpful message when mDNS lookup throws a SocketException',
() async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[],
<String, List<SrvResourceRecord>>{},
socketExceptionOnLookup: true,
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
expect(
() async => portDiscovery.firstMatchingVmService(client),
throwsToolExit(
message:
'Flutter could not access the local network.\n'
'\n'
'Please ensure your IDE or terminal app has permission to access '
'devices on the local network. This allows Flutter to connect to '
'the Dart VM.\n'
'\n'
'You can grant this permission in System Settings > Privacy & '
'Security > Local Network.\n',
),
);
},
// [intended] This tool exit message only works for macOS
skip: !globals.platform.isMacOS,
);
// On macOS, the mDNS client's socket stream creates a SocketException if
// the app running the tool does not have Local Network permissions.
// See: https://github.com/flutter/flutter/issues/150131
test(
'On macOS, tool exits with a helpful message when mDNS lookup throws an uncaught SocketException',
() async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[],
<String, List<SrvResourceRecord>>{},
uncaughtSocketExceptionOnLookup: true,
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
expect(
() async => portDiscovery.firstMatchingVmService(client),
throwsToolExit(
message:
'Flutter could not access the local network.\n'
'\n'
'Please ensure your IDE or terminal app has permission to access '
'devices on the local network. This allows Flutter to connect to '
'the Dart VM.\n'
'\n'
'You can grant this permission in System Settings > Privacy & '
'Security > Local Network.\n',
),
);
},
// [intended] This tool exit message only works for macOS
skip: !globals.platform.isMacOS,
);
test(
'On macOS, tool prints a helpful message when mDNS lookup throws an uncaught SocketException',
() async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[],
<String, List<SrvResourceRecord>>{},
uncaughtSocketExceptionOnLookup: true,
);
final BufferLogger logger = BufferLogger.test();
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: logger,
analytics: const NoOpAnalytics(),
);
final MDnsVmServiceDiscoveryResult? result = await portDiscovery.firstMatchingVmService(
client,
throwOnMissingLocalNetworkPermissionsError: false,
);
expect(result, isNull);
expect(
logger.errorText,
contains(
'Flutter could not access the local network.\n'
'\n'
'Please ensure your IDE or terminal app has permission to access '
'devices on the local network. This allows Flutter to connect to '
'the Dart VM.\n'
'\n'
'You can grant this permission in System Settings > Privacy & '
'Security > Local Network.\n',
),
);
},
// [intended] This tool exit message only works for macOS
skip: !globals.platform.isMacOS,
);
testWithoutContext('Correctly builds VM Service URI with hostVmservicePort == 0', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[PtrResourceRecord('foo', future, domainName: 'bar')],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
],
},
);
final FakeIOSDevice device = FakeIOSDevice();
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
final Uri? uri = await portDiscovery.getVMServiceUriForLaunch(
'bar',
device,
hostVmservicePort: 0,
deviceVmservicePort: 123,
);
expect(uri.toString(), 'http://127.0.0.1:123/');
});
testWithoutContext('Get wireless device IP (iPv4)', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[PtrResourceRecord('foo', future, domainName: 'bar')],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'),
],
},
ipResponse: <String, List<IPAddressResourceRecord>>{
'appId': <IPAddressResourceRecord>[
IPAddressResourceRecord(
'Device IP',
0,
address: InternetAddress.tryParse('111.111.111.111')!,
),
],
},
txtResponse: <String, List<TxtResourceRecord>>{
'bar': <TxtResourceRecord>[TxtResourceRecord('bar', future, text: 'authCode=xyz\n')],
},
);
final FakeIOSDevice device = FakeIOSDevice();
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
final Uri? uri = await portDiscovery.getVMServiceUriForLaunch(
'bar',
device,
useDeviceIPAsHost: true,
deviceVmservicePort: 1234,
);
expect(uri.toString(), 'http://111.111.111.111:1234/xyz/');
});
testWithoutContext('Get wireless device IP (iPv6)', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[PtrResourceRecord('foo', future, domainName: 'bar')],
<String, List<SrvResourceRecord>>{
'bar': <SrvResourceRecord>[
SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'),
],
},
ipResponse: <String, List<IPAddressResourceRecord>>{
'appId': <IPAddressResourceRecord>[
IPAddressResourceRecord(
'Device IP',
0,
address: InternetAddress.tryParse('1111:1111:1111:1111:1111:1111:1111:1111')!,
),
],
},
txtResponse: <String, List<TxtResourceRecord>>{
'bar': <TxtResourceRecord>[TxtResourceRecord('bar', future, text: 'authCode=xyz\n')],
},
);
final FakeIOSDevice device = FakeIOSDevice();
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
final Uri? uri = await portDiscovery.getVMServiceUriForLaunch(
'bar',
device,
useDeviceIPAsHost: true,
deviceVmservicePort: 1234,
);
expect(uri.toString(), 'http://[1111:1111:1111:1111:1111:1111:1111:1111]:1234/xyz/');
});
testWithoutContext(
'Throw error if unable to find VM Service with app id and device port',
() async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', future, domainName: 'srv-foo'),
PtrResourceRecord('bar', future, domainName: 'srv-bar'),
PtrResourceRecord('baz', future, domainName: 'srv-boo'),
],
<String, List<SrvResourceRecord>>{
'srv-foo': <SrvResourceRecord>[
SrvResourceRecord(
'srv-foo',
future,
port: 123,
weight: 1,
priority: 1,
target: 'target-foo',
),
],
'srv-bar': <SrvResourceRecord>[
SrvResourceRecord(
'srv-bar',
future,
port: 123,
weight: 1,
priority: 1,
target: 'target-bar',
),
],
'srv-baz': <SrvResourceRecord>[
SrvResourceRecord(
'srv-baz',
future,
port: 123,
weight: 1,
priority: 1,
target: 'target-baz',
),
],
},
);
final FakeIOSDevice device = FakeIOSDevice();
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
expect(
portDiscovery.getVMServiceUriForLaunch('srv-bar', device, deviceVmservicePort: 321),
throwsToolExit(
message: 'Did not find a Dart VM Service advertised for srv-bar on port 321.',
),
);
},
);
testWithoutContext('Matches on application id and device name', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', future, domainName: 'srv-foo'),
PtrResourceRecord('bar', future, domainName: 'srv-bar'),
PtrResourceRecord('baz', future, domainName: 'srv-boo'),
],
<String, List<SrvResourceRecord>>{
'srv-bar': <SrvResourceRecord>[
SrvResourceRecord(
'srv-foo',
future,
port: 123,
weight: 1,
priority: 1,
target: 'My-Phone.local',
),
],
},
);
final FakeIOSDevice device = FakeIOSDevice(name: 'My Phone');
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
final Uri? uri = await portDiscovery.getVMServiceUriForLaunch('srv-bar', device);
expect(uri.toString(), 'http://127.0.0.1:123/');
});
testWithoutContext(
'Throw error if unable to find VM Service with app id and device name',
() async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', future, domainName: 'srv-foo'),
PtrResourceRecord('bar', future, domainName: 'srv-bar'),
PtrResourceRecord('baz', future, domainName: 'srv-boo'),
],
<String, List<SrvResourceRecord>>{
'srv-foo': <SrvResourceRecord>[
SrvResourceRecord(
'srv-foo',
future,
port: 123,
weight: 1,
priority: 1,
target: 'target-foo',
),
],
},
);
final FakeIOSDevice device = FakeIOSDevice(name: 'My Phone');
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
expect(
portDiscovery.getVMServiceUriForLaunch('srv-bar', device),
throwsToolExit(message: 'Did not find a Dart VM Service advertised for srv-bar'),
);
},
);
});
group('deviceNameMatchesTargetName', () {
testWithoutContext('compares case insensitive and without spaces, hyphens, .local', () {
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}),
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
expect(portDiscovery.deviceNameMatchesTargetName('My phone', 'My-Phone.local'), isTrue);
});
testWithoutContext('includes numbers in comparison', () {
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}),
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
expect(portDiscovery.deviceNameMatchesTargetName('My phone', 'My-Phone-2.local'), isFalse);
});
});
testWithoutContext(
'Find firstMatchingVmService with many available and no application id',
() async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', future, domainName: 'srv-foo'),
PtrResourceRecord('bar', future, domainName: 'srv-bar'),
PtrResourceRecord('baz', future, domainName: 'srv-boo'),
],
<String, List<SrvResourceRecord>>{
'srv-foo': <SrvResourceRecord>[
SrvResourceRecord(
'srv-foo',
future,
port: 123,
weight: 1,
priority: 1,
target: 'target-foo',
),
],
'srv-bar': <SrvResourceRecord>[
SrvResourceRecord(
'srv-bar',
future,
port: 123,
weight: 1,
priority: 1,
target: 'target-bar',
),
],
'srv-baz': <SrvResourceRecord>[
SrvResourceRecord(
'srv-baz',
future,
port: 123,
weight: 1,
priority: 1,
target: 'target-baz',
),
],
},
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
final MDnsVmServiceDiscoveryResult? result = await portDiscovery.firstMatchingVmService(
client,
);
expect(result?.domainName, 'srv-foo');
},
);
testWithoutContext('Find firstMatchingVmService app id', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[
PtrResourceRecord('foo', future, domainName: 'srv-foo'),
PtrResourceRecord('bar', future, domainName: 'srv-bar'),
PtrResourceRecord('baz', future, domainName: 'srv-boo'),
],
<String, List<SrvResourceRecord>>{
'srv-foo': <SrvResourceRecord>[
SrvResourceRecord(
'srv-foo',
future,
port: 111,
weight: 1,
priority: 1,
target: 'target-foo',
),
],
'srv-bar': <SrvResourceRecord>[
SrvResourceRecord(
'srv-bar',
future,
port: 222,
weight: 1,
priority: 1,
target: 'target-bar',
),
SrvResourceRecord(
'srv-bar',
future,
port: 333,
weight: 1,
priority: 1,
target: 'target-bar-2',
),
],
'srv-baz': <SrvResourceRecord>[
SrvResourceRecord(
'srv-baz',
future,
port: 444,
weight: 1,
priority: 1,
target: 'target-baz',
),
],
},
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
final MDnsVmServiceDiscoveryResult? result = await portDiscovery.firstMatchingVmService(
client,
applicationId: 'srv-bar',
);
expect(result?.domainName, 'srv-bar');
expect(result?.port, 222);
});
testWithoutContext('find with no txt record', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[PtrResourceRecord('foo', future, domainName: 'srv-foo')],
<String, List<SrvResourceRecord>>{
'srv-foo': <SrvResourceRecord>[
SrvResourceRecord(
'srv-foo',
future,
port: 111,
weight: 1,
priority: 1,
target: 'target-foo',
),
],
},
ipResponse: <String, List<IPAddressResourceRecord>>{
'target-foo': <IPAddressResourceRecord>[
IPAddressResourceRecord(
'target-foo',
0,
address: InternetAddress.tryParse('111.111.111.111')!,
),
],
},
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
final MDnsVmServiceDiscoveryResult? result = await portDiscovery.firstMatchingVmService(
client,
applicationId: 'srv-foo',
useDeviceIPAsHost: true,
);
expect(result?.domainName, 'srv-foo');
expect(result?.port, 111);
expect(result?.authCode, '');
expect(result?.ipAddress?.address, '111.111.111.111');
});
testWithoutContext('find with empty txt record', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[PtrResourceRecord('foo', future, domainName: 'srv-foo')],
<String, List<SrvResourceRecord>>{
'srv-foo': <SrvResourceRecord>[
SrvResourceRecord(
'srv-foo',
future,
port: 111,
weight: 1,
priority: 1,
target: 'target-foo',
),
],
},
txtResponse: <String, List<TxtResourceRecord>>{
'srv-foo': <TxtResourceRecord>[TxtResourceRecord('srv-foo', future, text: '')],
},
ipResponse: <String, List<IPAddressResourceRecord>>{
'target-foo': <IPAddressResourceRecord>[
IPAddressResourceRecord(
'target-foo',
0,
address: InternetAddress.tryParse('111.111.111.111')!,
),
],
},
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
final MDnsVmServiceDiscoveryResult? result = await portDiscovery.firstMatchingVmService(
client,
applicationId: 'srv-foo',
useDeviceIPAsHost: true,
);
expect(result?.domainName, 'srv-foo');
expect(result?.port, 111);
expect(result?.authCode, '');
expect(result?.ipAddress?.address, '111.111.111.111');
});
testWithoutContext('find with valid txt record', () async {
final MDnsClient client = FakeMDnsClient(
<PtrResourceRecord>[PtrResourceRecord('foo', future, domainName: 'srv-foo')],
<String, List<SrvResourceRecord>>{
'srv-foo': <SrvResourceRecord>[
SrvResourceRecord(
'srv-foo',
future,
port: 111,
weight: 1,
priority: 1,
target: 'target-foo',
),
],
},
txtResponse: <String, List<TxtResourceRecord>>{
'srv-foo': <TxtResourceRecord>[
TxtResourceRecord('srv-foo', future, text: 'authCode=xyz\n'),
],
},
ipResponse: <String, List<IPAddressResourceRecord>>{
'target-foo': <IPAddressResourceRecord>[
IPAddressResourceRecord(
'target-foo',
0,
address: InternetAddress.tryParse('111.111.111.111')!,
),
],
},
);
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
mdnsClient: client,
logger: BufferLogger.test(),
analytics: const NoOpAnalytics(),
);
final MDnsVmServiceDiscoveryResult? result = await portDiscovery.firstMatchingVmService(
client,
applicationId: 'srv-foo',
useDeviceIPAsHost: true,
);
expect(result?.domainName, 'srv-foo');
expect(result?.port, 111);
expect(result?.authCode, 'xyz/');
expect(result?.ipAddress?.address, '111.111.111.111');
});
});
}
class FakeMDnsClient extends Fake implements MDnsClient {
FakeMDnsClient(
this.ptrRecords,
this.srvResponse, {
this.txtResponse = const <String, List<TxtResourceRecord>>{},
this.ipResponse = const <String, List<IPAddressResourceRecord>>{},
this.osErrorOnStart = false,
this.socketExceptionOnLookup = false,
this.uncaughtSocketExceptionOnLookup = false,
});
final List<PtrResourceRecord> ptrRecords;
final Map<String, List<SrvResourceRecord>> srvResponse;
final Map<String, List<TxtResourceRecord>> txtResponse;
final Map<String, List<IPAddressResourceRecord>> ipResponse;
final bool osErrorOnStart;
final bool socketExceptionOnLookup;
final bool uncaughtSocketExceptionOnLookup;
@override
Future<void> start({
InternetAddress? listenAddress,
NetworkInterfacesFactory? interfacesFactory,
int mDnsPort = 5353,
InternetAddress? mDnsAddress,
Function? onError,
}) async {
if (osErrorOnStart) {
throw const OSError('Operation not supported on socket', 102);
}
}
@override
Stream<T> lookup<T extends ResourceRecord>(
ResourceRecordQuery query, {
Duration timeout = const Duration(seconds: 5),
}) {
if (socketExceptionOnLookup) {
throw const SocketException('Socket Exception');
}
if (uncaughtSocketExceptionOnLookup) {
Zone.current.handleUncaughtError(
const SocketException('Socket Exception'),
StackTrace.current,
);
}
if (T == PtrResourceRecord &&
query.fullyQualifiedName == MDnsVmServiceDiscovery.dartVmServiceName) {
return Stream<PtrResourceRecord>.fromIterable(ptrRecords) as Stream<T>;
}
if (T == SrvResourceRecord) {
final String key = query.fullyQualifiedName;
return Stream<SrvResourceRecord>.fromIterable(srvResponse[key] ?? <SrvResourceRecord>[])
as Stream<T>;
}
if (T == TxtResourceRecord) {
final String key = query.fullyQualifiedName;
return Stream<TxtResourceRecord>.fromIterable(txtResponse[key] ?? <TxtResourceRecord>[])
as Stream<T>;
}
if (T == IPAddressResourceRecord) {
final String key = query.fullyQualifiedName;
return Stream<IPAddressResourceRecord>.fromIterable(
ipResponse[key] ?? <IPAddressResourceRecord>[],
)
as Stream<T>;
}
throw UnsupportedError('Unsupported query type $T');
}
@override
void stop() {}
}
class FakeIOSDevice extends Fake implements IOSDevice {
FakeIOSDevice({this.name = 'iPhone'});
@override
final String name;
@override
Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios;
@override
bool isSupported() => true;
@override
bool isSupportedForProject(FlutterProject flutterProject) => true;
@override
DevicePortForwarder get portForwarder => const NoOpDevicePortForwarder();
}