Unverified Commit ee42a302 authored by Victoria Ashworth's avatar Victoria Ashworth Committed by GitHub

Move target devices logic to its own classes and file (#121903)

Move target devices logic to its own classes and file
parent 2d1a5cdc
......@@ -62,9 +62,27 @@ class DevicesCommand extends FlutterCommand {
exitCode: 1);
}
final DevicesCommandOutput output = DevicesCommandOutput(
deviceDiscoveryTimeout: deviceDiscoveryTimeout,
);
await output.findAndOutputAllTargetDevices(
machine: boolArgDeprecated('machine'),
);
return FlutterCommandResult.success();
}
}
class DevicesCommandOutput {
DevicesCommandOutput({this.deviceDiscoveryTimeout});
final Duration? deviceDiscoveryTimeout;
Future<void> findAndOutputAllTargetDevices({required bool machine}) async {
final List<Device> devices = await globals.deviceManager?.refreshAllDevices(timeout: deviceDiscoveryTimeout) ?? <Device>[];
if (boolArgDeprecated('machine')) {
if (machine) {
await printDevicesAsJson(devices);
} else {
if (devices.isEmpty) {
......@@ -86,7 +104,6 @@ class DevicesCommand extends FlutterCommand {
}
await _printDiagnostics();
}
return FlutterCommandResult.success();
}
Future<void> _printDiagnostics() async {
......
......@@ -245,76 +245,11 @@ abstract class DeviceManager {
];
}
/// Find and return all target [Device]s based upon currently connected
/// devices, the current project, and criteria entered by the user on
/// the command line.
///
/// If no device can be found that meets specified criteria,
/// then print an error message and return null.
///
/// Returns a list of devices specified by the user.
///
/// * If the user specified '-d all', then return all connected devices which
/// support the current project, except for fuchsia and web.
///
/// * If the user specified a device id, then do nothing as the list is already
/// filtered by [getDevices].
///
/// * If the user did not specify a device id and there is more than one
/// device connected, then filter out unsupported devices and prioritize
/// ephemeral devices.
///
/// * If [promptUserToChooseDevice] is true, and there are more than one
/// device after the aforementioned filters, and the user is connected to a
/// terminal, then show a prompt asking the user to choose one.
Future<List<Device>> findTargetDevices({
bool includeDevicesUnsupportedByProject = false,
Duration? timeout,
}) async {
if (timeout != null) {
// Reset the cache with the specified timeout.
await refreshAllDevices(timeout: timeout);
}
final List<Device> devices = await getDevices(
filter: DeviceDiscoveryFilter(
supportFilter: deviceSupportFilter(
includeDevicesUnsupportedByProject: includeDevicesUnsupportedByProject,
),
),
);
if (!hasSpecifiedDeviceId) {
// User did not specify the device.
if (devices.length > 1) {
// If there are still multiple devices and the user did not specify to run
// all, then attempt to prioritize ephemeral devices. For example, if the
// user only typed 'flutter run' and both an Android device and desktop
// device are available, choose the Android device.
// Ephemeral is nullable for device types where this is not well
// defined.
final List<Device> ephemeralDevices = <Device>[
for (final Device device in devices)
if (device.ephemeral == true)
device,
];
if (ephemeralDevices.length == 1) {
return ephemeralDevices;
}
}
}
return devices;
}
/// Determines how to filter devices.
///
/// By default, filters to only include devices that are supported by Flutter.
///
/// If the user has not specificied a device, filters to only include devices
/// If the user has not specified a device, filters to only include devices
/// that are supported by Flutter and supported by the project.
///
/// If the user has specified `--device all`, filters to only include devices
......@@ -343,6 +278,27 @@ abstract class DeviceManager {
return DeviceDiscoverySupportFilter.excludeDevicesUnsupportedByFlutter();
}
}
/// If the user did not specify to run all or a specific device, then attempt
/// to prioritize ephemeral devices.
///
/// If there is not exactly one ephemeral device return null.
///
/// For example, if the user only typed 'flutter run' and both an Android
/// device and desktop device are available, choose the Android device.
///
/// Note: ephemeral is nullable for device types where this is not well
/// defined.
Device? getSingleEphemeralDevice(List<Device> devices){
if (!hasSpecifiedDeviceId) {
try {
return devices.singleWhere((Device device) => device.ephemeral == true);
} on StateError {
return null;
}
}
return null;
}
}
/// A class for determining how to filter devices based on if they are supported.
......
......@@ -31,6 +31,7 @@ import '../project.dart';
import '../reporting/reporting.dart';
import '../web/compile.dart';
import 'flutter_command_runner.dart';
import 'target_devices.dart';
export '../cache.dart' show DevelopmentArtifact;
......@@ -709,6 +710,11 @@ abstract class FlutterCommand extends Command<void> {
return null;
}();
late final TargetDevices _targetDevices = TargetDevices(
deviceManager: globals.deviceManager!,
logger: globals.logger,
);
void addBuildModeFlags({
required bool verboseHelp,
bool defaultToRelease = true,
......@@ -1530,113 +1536,10 @@ abstract class FlutterCommand extends Command<void> {
Future<List<Device>?> findAllTargetDevices({
bool includeDevicesUnsupportedByProject = false,
}) async {
if (!globals.doctor!.canLaunchAnything) {
globals.printError(userMessages.flutterNoDevelopmentDevice);
return null;
}
final DeviceManager deviceManager = globals.deviceManager!;
List<Device> devices = await deviceManager.findTargetDevices(
return _targetDevices.findAllTargetDevices(
deviceDiscoveryTimeout: deviceDiscoveryTimeout,
includeDevicesUnsupportedByProject: includeDevicesUnsupportedByProject,
timeout: deviceDiscoveryTimeout,
);
if (devices.isEmpty) {
if (deviceManager.hasSpecifiedDeviceId) {
globals.logger.printStatus(userMessages.flutterNoMatchingDevice(deviceManager.specifiedDeviceId!));
final List<Device> allDevices = await deviceManager.getAllDevices();
if (allDevices.isNotEmpty) {
globals.logger.printStatus('');
globals.logger.printStatus('The following devices were found:');
await Device.printDevices(allDevices, globals.logger);
}
return null;
} else if (deviceManager.hasSpecifiedAllDevices) {
globals.logger.printStatus(userMessages.flutterNoDevicesFound);
await _printUnsupportedDevice(deviceManager);
return null;
} else {
globals.logger.printStatus(userMessages.flutterNoSupportedDevices);
await _printUnsupportedDevice(deviceManager);
return null;
}
} else if (devices.length > 1) {
if (deviceManager.hasSpecifiedDeviceId) {
globals.logger.printStatus(userMessages.flutterFoundSpecifiedDevices(devices.length, deviceManager.specifiedDeviceId!));
return null;
} else if (!deviceManager.hasSpecifiedAllDevices) {
if (globals.terminal.stdinHasTerminal) {
// If DeviceManager was not able to prioritize a device. For example, if the user
// has two active Android devices running, then we request the user to
// choose one. If the user has two nonEphemeral devices running, we also
// request input to choose one.
globals.logger.printStatus(userMessages.flutterMultipleDevicesFound);
await Device.printDevices(devices, globals.logger);
final Device chosenDevice = await _chooseOneOfAvailableDevices(devices);
// Update the [DeviceManager.specifiedDeviceId] so that we will not be prompted again.
deviceManager.specifiedDeviceId = chosenDevice.id;
devices = <Device>[chosenDevice];
} else {
// Show an error message asking the user to specify `-d all` if they
// want to run on multiple devices.
final List<Device> allDevices = await deviceManager.getAllDevices();
globals.logger.printStatus(userMessages.flutterSpecifyDeviceWithAllOption);
globals.logger.printStatus('');
await Device.printDevices(allDevices, globals.logger);
return null;
}
}
}
return devices;
}
Future<void> _printUnsupportedDevice(DeviceManager deviceManager) async {
final List<Device> unsupportedDevices = await deviceManager.getDevices();
if (unsupportedDevices.isNotEmpty) {
final StringBuffer result = StringBuffer();
result.writeln(userMessages.flutterFoundButUnsupportedDevices);
result.writeAll(
(await Device.descriptions(unsupportedDevices))
.map((String desc) => desc)
.toList(),
'\n',
);
result.writeln();
result.writeln(userMessages.flutterMissPlatformProjects(
Device.devicesPlatformTypes(unsupportedDevices),
));
globals.logger.printStatus(result.toString());
}
}
Future<Device> _chooseOneOfAvailableDevices(List<Device> devices) async {
_displayDeviceOptions(devices);
final String userInput = await _readUserInput(devices.length);
if (userInput.toLowerCase() == 'q') {
throwToolExit('');
}
return devices[int.parse(userInput) - 1];
}
void _displayDeviceOptions(List<Device> devices) {
int count = 1;
for (final Device device in devices) {
globals.logger.printStatus(userMessages.flutterChooseDevice(count, device.name, device.id));
count++;
}
}
Future<String> _readUserInput(int deviceCount) async {
globals.terminal.usesTerminalUi = true;
final String result = await globals.terminal.promptForCharInput(
<String>[ for (int i = 0; i < deviceCount; i++) '${i + 1}', 'q', 'Q'],
displayAcceptedCharacters: false,
logger: globals.logger,
prompt: userMessages.flutterChooseOne,
);
return result;
}
/// Find and return the target [Device] based upon currently connected
......
// 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 'package:meta/meta.dart';
import '../base/common.dart';
import '../base/logger.dart';
import '../base/user_messages.dart';
import '../device.dart';
import '../globals.dart' as globals;
class TargetDevices {
TargetDevices({
required DeviceManager deviceManager,
required Logger logger,
}) : _deviceManager = deviceManager,
_logger = logger;
final DeviceManager _deviceManager;
final Logger _logger;
/// Find and return all target [Device]s based upon currently connected
/// devices and criteria entered by the user on the command line.
/// If no device can be found that meets specified criteria,
/// then print an error message and return null.
Future<List<Device>?> findAllTargetDevices({
Duration? deviceDiscoveryTimeout,
bool includeDevicesUnsupportedByProject = false,
}) async {
if (!globals.doctor!.canLaunchAnything) {
_logger.printError(userMessages.flutterNoDevelopmentDevice);
return null;
}
List<Device> devices = await getDevices(
includeDevicesUnsupportedByProject: includeDevicesUnsupportedByProject,
timeout: deviceDiscoveryTimeout,
);
if (devices.isEmpty) {
if (_deviceManager.hasSpecifiedDeviceId) {
_logger.printStatus(userMessages.flutterNoMatchingDevice(_deviceManager.specifiedDeviceId!));
final List<Device> allDevices = await _deviceManager.getAllDevices();
if (allDevices.isNotEmpty) {
_logger.printStatus('');
_logger.printStatus('The following devices were found:');
await Device.printDevices(allDevices, _logger);
}
return null;
} else if (_deviceManager.hasSpecifiedAllDevices) {
_logger.printStatus(userMessages.flutterNoDevicesFound);
await _printUnsupportedDevice(_deviceManager);
return null;
} else {
_logger.printStatus(userMessages.flutterNoSupportedDevices);
await _printUnsupportedDevice(_deviceManager);
return null;
}
} else if (devices.length > 1) {
if (_deviceManager.hasSpecifiedDeviceId) {
_logger.printStatus(userMessages.flutterFoundSpecifiedDevices(devices.length, _deviceManager.specifiedDeviceId!));
return null;
} else if (!_deviceManager.hasSpecifiedAllDevices) {
if (globals.terminal.stdinHasTerminal) {
// If DeviceManager was not able to prioritize a device. For example, if the user
// has two active Android devices running, then we request the user to
// choose one. If the user has two nonEphemeral devices running, we also
// request input to choose one.
_logger.printStatus(userMessages.flutterMultipleDevicesFound);
await Device.printDevices(devices, _logger);
final Device chosenDevice = await _chooseOneOfAvailableDevices(devices);
// Update the [DeviceManager.specifiedDeviceId] so that we will not be prompted again.
_deviceManager.specifiedDeviceId = chosenDevice.id;
devices = <Device>[chosenDevice];
} else {
// Show an error message asking the user to specify `-d all` if they
// want to run on multiple devices.
final List<Device> allDevices = await _deviceManager.getAllDevices();
_logger.printStatus(userMessages.flutterSpecifyDeviceWithAllOption);
_logger.printStatus('');
await Device.printDevices(allDevices, _logger);
return null;
}
}
}
return devices;
}
Future<void> _printUnsupportedDevice(DeviceManager deviceManager) async {
final List<Device> unsupportedDevices = await deviceManager.getDevices();
if (unsupportedDevices.isNotEmpty) {
final StringBuffer result = StringBuffer();
result.writeln(userMessages.flutterFoundButUnsupportedDevices);
result.writeAll(
(await Device.descriptions(unsupportedDevices))
.map((String desc) => desc)
.toList(),
'\n',
);
result.writeln();
result.writeln(userMessages.flutterMissPlatformProjects(
Device.devicesPlatformTypes(unsupportedDevices),
));
_logger.printStatus(result.toString());
}
}
Future<Device> _chooseOneOfAvailableDevices(List<Device> devices) async {
_displayDeviceOptions(devices);
final String userInput = await _readUserInput(devices.length);
if (userInput.toLowerCase() == 'q') {
throwToolExit('');
}
return devices[int.parse(userInput) - 1];
}
void _displayDeviceOptions(List<Device> devices) {
int count = 1;
for (final Device device in devices) {
_logger.printStatus(userMessages.flutterChooseDevice(count, device.name, device.id));
count++;
}
}
Future<String> _readUserInput(int deviceCount) async {
globals.terminal.usesTerminalUi = true;
final String result = await globals.terminal.promptForCharInput(
<String>[ for (int i = 0; i < deviceCount; i++) '${i + 1}', 'q', 'Q'],
displayAcceptedCharacters: false,
logger: _logger,
prompt: userMessages.flutterChooseOne,
);
return result;
}
/// Find and return all target [Device]s based upon currently connected
/// devices, the current project, and criteria entered by the user on
/// the command line.
///
/// Returns a list of devices specified by the user.
///
/// * If the user specified '-d all', then return all connected devices which
/// support the current project, except for fuchsia and web.
///
/// * If the user specified a device id, then do nothing as the list is already
/// filtered by [_deviceManager.getDevices].
///
/// * If the user did not specify a device id and there is more than one
/// device connected, then filter out unsupported devices and prioritize
/// ephemeral devices.
@visibleForTesting
Future<List<Device>> getDevices({
bool includeDevicesUnsupportedByProject = false,
Duration? timeout,
}) async {
if (timeout != null) {
// Reset the cache with the specified timeout.
await _deviceManager.refreshAllDevices(timeout: timeout);
}
final List<Device> devices = await _deviceManager.getDevices(
filter: DeviceDiscoveryFilter(
supportFilter: _deviceManager.deviceSupportFilter(
includeDevicesUnsupportedByProject: includeDevicesUnsupportedByProject,
),
),
);
// If there is more than one device, attempt to prioritize ephemeral devices.
if (devices.length > 1) {
final Device? ephemeralDevice = _deviceManager.getSingleEphemeralDevice(devices);
if (ephemeralDevice != null) {
return <Device>[ephemeralDevice];
}
}
return devices;
}
}
......@@ -581,25 +581,6 @@ class FakePub extends Fake implements Pub {
}) async { }
}
class FakeDeviceManager extends Fake implements DeviceManager {
List<Device> devices = <Device>[];
@override
String? specifiedDeviceId;
@override
Future<List<Device>> getDevices({
DeviceDiscoveryFilter? filter,
}) async => devices;
@override
Future<List<Device>> findTargetDevices({
bool includeDevicesUnsupportedByProject = false,
Duration? timeout,
bool promptUserToChooseDevice = true,
}) async => devices;
}
/// A [FlutterDriverFactory] that creates a [NeverEndingDriverService].
class NeverEndingFlutterDriverFactory extends Fake implements FlutterDriverFactory {
NeverEndingFlutterDriverFactory(this.callback);
......
......@@ -11,9 +11,7 @@ import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/error_handling_io.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/signals.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/base/time.dart';
import 'package:flutter_tools/src/base/user_messages.dart';
import 'package:flutter_tools/src/build_info.dart';
......@@ -696,135 +694,31 @@ void main() {
ProcessManager: () => processManager,
});
group('findAllTargetDevices', () {
group('findTargetDevice', () {
final FakeDevice device1 = FakeDevice('device1', 'device1');
final FakeDevice device2 = FakeDevice('device2', 'device2');
group('when specified device id', () {
testUsingContext('returns device when device is found', () async {
testDeviceManager.specifiedDeviceId = 'device-id';
testDeviceManager.addDevice(device1);
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final List<Device>? devices = await flutterCommand.findAllTargetDevices();
expect(devices, <Device>[device1]);
});
testUsingContext('show error when no device found', () async {
testDeviceManager.specifiedDeviceId = 'device-id';
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final List<Device>? devices = await flutterCommand.findAllTargetDevices();
expect(devices, null);
expect(testLogger.statusText, contains(UserMessages().flutterNoMatchingDevice('device-id')));
});
testUsingContext('show error when multiple devices found', () async {
testDeviceManager.specifiedDeviceId = 'device-id';
testDeviceManager.addDevice(device1);
testDeviceManager.addDevice(device2);
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final List<Device>? devices = await flutterCommand.findAllTargetDevices();
expect(devices, null);
expect(testLogger.statusText, contains(UserMessages().flutterFoundSpecifiedDevices(2, 'device-id')));
});
testUsingContext('no device found', () async {
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final Device? device = await flutterCommand.findTargetDevice();
expect(device, isNull);
});
group('when specified all', () {
testUsingContext('can return one device', () async {
testDeviceManager.specifiedDeviceId = 'all';
testDeviceManager.addDevice(device1);
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final List<Device>? devices = await flutterCommand.findAllTargetDevices();
expect(devices, <Device>[device1]);
});
testUsingContext('can return multiple devices', () async {
testDeviceManager.specifiedDeviceId = 'all';
testDeviceManager.addDevice(device1);
testDeviceManager.addDevice(device2);
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final List<Device>? devices = await flutterCommand.findAllTargetDevices();
expect(devices, <Device>[device1, device2]);
});
testUsingContext('show error when no device found', () async {
testDeviceManager.specifiedDeviceId = 'all';
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final List<Device>? devices = await flutterCommand.findAllTargetDevices();
expect(devices, null);
expect(testLogger.statusText, contains(UserMessages().flutterNoDevicesFound));
});
testUsingContext('finds single device', () async {
testDeviceManager.addDevice(device1);
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final Device? device = await flutterCommand.findTargetDevice();
expect(device, device1);
});
group('when device not specified', () {
testUsingContext('returns one device when only one device connected', () async {
testDeviceManager.addDevice(device1);
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final List<Device>? devices = await flutterCommand.findAllTargetDevices();
expect(devices, <Device>[device1]);
});
testUsingContext('show error when no device found', () async {
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final List<Device>? devices = await flutterCommand.findAllTargetDevices();
expect(devices, null);
expect(testLogger.statusText, contains(UserMessages().flutterNoSupportedDevices));
});
testUsingContext('show error when multiple devices found and not connected to terminal', () async {
testDeviceManager.addDevice(device1);
testDeviceManager.addDevice(device2);
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final List<Device>? devices = await flutterCommand.findAllTargetDevices();
expect(devices, null);
expect(testLogger.statusText, contains(UserMessages().flutterSpecifyDeviceWithAllOption));
}, overrides: <Type, Generator>{
AnsiTerminal: () => FakeTerminal(stdinHasTerminal: false),
});
// Prompt to choose device when multiple devices found and connected to terminal
group('show prompt', () {
late FakeTerminal terminal;
setUp(() {
terminal = FakeTerminal();
});
testUsingContext('choose first device', () async {
testDeviceManager.addDevice(device1);
testDeviceManager.addDevice(device2);
terminal.setPrompt(<String>['1', '2', 'q', 'Q'], '1');
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final List<Device>? devices = await flutterCommand.findAllTargetDevices();
expect(devices, <Device>[device1]);
}, overrides: <Type, Generator>{
AnsiTerminal: () => terminal,
});
testUsingContext('choose second device', () async {
testDeviceManager.addDevice(device1);
testDeviceManager.addDevice(device2);
terminal.setPrompt(<String>['1', '2', 'q', 'Q'], '2');
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final List<Device>? devices = await flutterCommand.findAllTargetDevices();
expect(devices, <Device>[device2]);
}, overrides: <Type, Generator>{
AnsiTerminal: () => terminal,
});
testUsingContext('exits without choosing device', () async {
testDeviceManager.addDevice(device1);
testDeviceManager.addDevice(device2);
terminal.setPrompt(<String>['1', '2', 'q', 'Q'], 'q');
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
await expectLater(
flutterCommand.findAllTargetDevices(),
throwsToolExit(),
);
}, overrides: <Type, Generator>{
AnsiTerminal: () => terminal,
});
});
testUsingContext('finds multiple devices', () async {
testDeviceManager.addDevice(device1);
testDeviceManager.addDevice(device2);
testDeviceManager.specifiedDeviceId = 'all';
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final Device? device = await flutterCommand.findTargetDevice();
expect(device, isNull);
expect(testLogger.statusText, contains(UserMessages().flutterSpecifyDevice));
});
});
});
......@@ -980,33 +874,3 @@ class FakePub extends Fake implements Pub {
PubOutputMode outputMode = PubOutputMode.all,
}) async { }
}
class FakeTerminal extends Fake implements AnsiTerminal {
FakeTerminal({this.stdinHasTerminal = true});
@override
final bool stdinHasTerminal;
@override
bool usesTerminalUi = true;
void setPrompt(List<String> characters, String result) {
_nextPrompt = characters;
_nextResult = result;
}
List<String>? _nextPrompt;
late String _nextResult;
@override
Future<String> promptForCharInput(
List<String> acceptedCharacters, {
Logger? logger,
String? prompt,
int? defaultChoiceIndex,
bool displayAcceptedCharacters = true,
}) async {
expect(acceptedCharacters, _nextPrompt);
return _nextResult;
}
}
......@@ -222,7 +222,9 @@ class FakeDeviceManager implements DeviceManager {
String deviceId, {
DeviceDiscoveryFilter? filter,
}) async {
return devices.where((Device device) => device.id == deviceId).toList();
return devices.where((Device device) {
return device.id == deviceId || device.id.startsWith(deviceId);
}).toList();
}
@override
......@@ -245,15 +247,6 @@ class FakeDeviceManager implements DeviceManager {
@override
List<DeviceDiscovery> get deviceDiscoverers => <DeviceDiscovery>[];
@override
Future<List<Device>> findTargetDevices({
bool includeDevicesUnsupportedByProject = false,
Duration? timeout,
bool promptUserToChooseDevice = true,
}) async {
return devices;
}
@override
DeviceDiscoverySupportFilter deviceSupportFilter({
bool includeDevicesUnsupportedByProject = false,
......@@ -261,6 +254,9 @@ class FakeDeviceManager implements DeviceManager {
}) {
return TestDeviceDiscoverySupportFilter();
}
@override
Device? getSingleEphemeralDevice(List<Device> devices) => null;
}
class TestDeviceDiscoverySupportFilter extends Fake implements DeviceDiscoverySupportFilter {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment