Unverified Commit 1957c663 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

add testing to screenshot and printDetails method (#36418)

parent b7a49c4a
...@@ -679,7 +679,14 @@ abstract class ResidentRunner { ...@@ -679,7 +679,14 @@ abstract class ResidentRunner {
} }
} }
/// Take a screenshot on the provided [device].
///
/// If the device has a connected vmservice, this method will attempt to hide
/// and restore the debug banner before taking the screenshot.
///
/// Throws an [AssertionError] if [Devce.supportsScreenshot] is not true.
Future<void> screenshot(FlutterDevice device) async { Future<void> screenshot(FlutterDevice device) async {
assert(device.device.supportsScreenshot);
final Status status = logger.startProgress('Taking screenshot for ${device.device.name}...', timeout: timeoutConfiguration.fastOperation); final Status status = logger.startProgress('Taking screenshot for ${device.device.name}...', timeout: timeoutConfiguration.fastOperation);
final File outputFile = getUniqueFile(fs.currentDirectory, 'flutter', 'png'); final File outputFile = getUniqueFile(fs.currentDirectory, 'flutter', 'png');
try { try {
......
...@@ -5,6 +5,9 @@ ...@@ -5,6 +5,9 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device.dart';
...@@ -19,233 +22,335 @@ import '../src/common.dart'; ...@@ -19,233 +22,335 @@ import '../src/common.dart';
import '../src/testbed.dart'; import '../src/testbed.dart';
void main() { void main() {
group('ResidentRunner', () { final Uri testUri = Uri.parse('foo://bar');
final Uri testUri = Uri.parse('foo://bar'); Testbed testbed;
Testbed testbed; MockFlutterDevice mockFlutterDevice;
MockFlutterDevice mockFlutterDevice; MockVMService mockVMService;
MockVMService mockVMService; MockDevFS mockDevFS;
MockDevFS mockDevFS; MockFlutterView mockFlutterView;
MockFlutterView mockFlutterView; ResidentRunner residentRunner;
ResidentRunner residentRunner; MockDevice mockDevice;
MockDevice mockDevice; MockIsolate mockIsolate;
MockIsolate mockIsolate;
setUp(() {
setUp(() { testbed = Testbed(setup: () {
testbed = Testbed(setup: () { residentRunner = HotRunner(
residentRunner = HotRunner( <FlutterDevice>[
<FlutterDevice>[ mockFlutterDevice,
mockFlutterDevice, ],
], stayResident: false,
stayResident: false, debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), );
); });
}); mockFlutterDevice = MockFlutterDevice();
mockFlutterDevice = MockFlutterDevice(); mockDevice = MockDevice();
mockDevice = MockDevice(); mockVMService = MockVMService();
mockVMService = MockVMService(); mockDevFS = MockDevFS();
mockDevFS = MockDevFS(); mockFlutterView = MockFlutterView();
mockFlutterView = MockFlutterView(); mockIsolate = MockIsolate();
mockIsolate = MockIsolate(); // DevFS Mocks
// DevFS Mocks when(mockDevFS.lastCompiled).thenReturn(DateTime(2000));
when(mockDevFS.lastCompiled).thenReturn(DateTime(2000)); when(mockDevFS.sources).thenReturn(<Uri>[]);
when(mockDevFS.sources).thenReturn(<Uri>[]); when(mockDevFS.destroy()).thenAnswer((Invocation invocation) async { });
when(mockDevFS.destroy()).thenAnswer((Invocation invocation) async { }); // FlutterDevice Mocks.
// FlutterDevice Mocks. when(mockFlutterDevice.updateDevFS(
when(mockFlutterDevice.updateDevFS( // Intentionally provide empty list to match above mock.
// Intentionally provide empty list to match above mock. invalidatedFiles: <Uri>[],
invalidatedFiles: <Uri>[], mainPath: anyNamed('mainPath'),
mainPath: anyNamed('mainPath'), target: anyNamed('target'),
target: anyNamed('target'), bundle: anyNamed('bundle'),
bundle: anyNamed('bundle'), firstBuildTime: anyNamed('firstBuildTime'),
firstBuildTime: anyNamed('firstBuildTime'), bundleFirstUpload: anyNamed('bundleFirstUpload'),
bundleFirstUpload: anyNamed('bundleFirstUpload'), bundleDirty: anyNamed('bundleDirty'),
bundleDirty: anyNamed('bundleDirty'), fullRestart: anyNamed('fullRestart'),
fullRestart: anyNamed('fullRestart'), projectRootPath: anyNamed('projectRootPath'),
projectRootPath: anyNamed('projectRootPath'), pathToReload: anyNamed('pathToReload'),
pathToReload: anyNamed('pathToReload'), )).thenAnswer((Invocation invocation) async {
)).thenAnswer((Invocation invocation) async { return UpdateFSReport(
return UpdateFSReport( success: true,
success: true, syncedBytes: 0,
syncedBytes: 0, invalidatedSourcesCount: 0,
invalidatedSourcesCount: 0, );
); });
when(mockFlutterDevice.devFS).thenReturn(mockDevFS);
when(mockFlutterDevice.views).thenReturn(<FlutterView>[
mockFlutterView
]);
when(mockFlutterDevice.device).thenReturn(mockDevice);
when(mockFlutterView.uiIsolate).thenReturn(mockIsolate);
when(mockFlutterDevice.stopEchoingDeviceLog()).thenAnswer((Invocation invocation) async { });
when(mockFlutterDevice.observatoryUris).thenReturn(<Uri>[
testUri,
]);
when(mockFlutterDevice.connect(
reloadSources: anyNamed('reloadSources'),
restart: anyNamed('restart'),
compileExpression: anyNamed('compileExpression')
)).thenAnswer((Invocation invocation) async { });
when(mockFlutterDevice.setupDevFS(any, any, packagesFilePath: anyNamed('packagesFilePath')))
.thenAnswer((Invocation invocation) async {
return testUri;
}); });
when(mockFlutterDevice.devFS).thenReturn(mockDevFS); when(mockFlutterDevice.vmServices).thenReturn(<VMService>[
when(mockFlutterDevice.views).thenReturn(<FlutterView>[ mockVMService,
mockFlutterView ]);
]); when(mockFlutterDevice.refreshViews()).thenAnswer((Invocation invocation) async { });
when(mockFlutterDevice.device).thenReturn(mockDevice); // VMService mocks.
when(mockFlutterView.uiIsolate).thenReturn(mockIsolate); when(mockVMService.wsAddress).thenReturn(testUri);
when(mockFlutterDevice.stopEchoingDeviceLog()).thenAnswer((Invocation invocation) async { }); when(mockVMService.done).thenAnswer((Invocation invocation) {
when(mockFlutterDevice.observatoryUris).thenReturn(<Uri>[ final Completer<void> result = Completer<void>.sync();
testUri, return result.future;
]); });
when(mockFlutterDevice.connect( when(mockIsolate.resume()).thenAnswer((Invocation invocation) {
reloadSources: anyNamed('reloadSources'), return Future<Map<String, Object>>.value(null);
restart: anyNamed('restart'), });
compileExpression: anyNamed('compileExpression') when(mockIsolate.flutterExit()).thenAnswer((Invocation invocation) {
)).thenAnswer((Invocation invocation) async { }); return Future<Map<String, Object>>.value(null);
when(mockFlutterDevice.setupDevFS(any, any, packagesFilePath: anyNamed('packagesFilePath'))) });
});
test('ResidentRunner can attach to device successfully', () => testbed.run(() async {
final Completer<DebugConnectionInfo> onConnectionInfo = Completer<DebugConnectionInfo>.sync();
final Completer<void> onAppStart = Completer<void>.sync();
final Future<int> result = residentRunner.attach(
appStartedCompleter: onAppStart,
connectionInfoCompleter: onConnectionInfo,
);
final Future<DebugConnectionInfo> connectionInfo = onConnectionInfo.future;
expect(await result, 0);
verify(mockFlutterDevice.initLogReader()).called(1);
expect(onConnectionInfo.isCompleted, true);
expect((await connectionInfo).baseUri, 'foo://bar');
expect(onAppStart.isCompleted, true);
}));
test('ResidentRunner can handle an RPC exception from hot reload', () => testbed.run(() async {
when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) async {
return 'Example';
});
when(mockDevice.targetPlatform).thenAnswer((Invocation invocation) async {
return TargetPlatform.android_arm;
});
when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) async {
return false;
});
final Completer<DebugConnectionInfo> onConnectionInfo = Completer<DebugConnectionInfo>.sync();
final Completer<void> onAppStart = Completer<void>.sync();
unawaited(residentRunner.attach(
appStartedCompleter: onAppStart,
connectionInfoCompleter: onConnectionInfo,
));
await onAppStart.future;
when(mockFlutterDevice.updateDevFS(
mainPath: anyNamed('mainPath'),
target: anyNamed('target'),
bundle: anyNamed('bundle'),
firstBuildTime: anyNamed('firstBuildTime'),
bundleFirstUpload: anyNamed('bundleFirstUpload'),
bundleDirty: anyNamed('bundleDirty'),
fullRestart: anyNamed('fullRestart'),
projectRootPath: anyNamed('projectRootPath'),
pathToReload: anyNamed('pathToReload'),
invalidatedFiles: anyNamed('invalidatedFiles'),
)).thenThrow(RpcException(666, 'something bad happened'));
final OperationResult result = await residentRunner.restart(fullRestart: false);
expect(result.fatal, true);
expect(result.code, 1);
verify(flutterUsage.sendEvent('hot', 'exception', parameters: <String, String>{
reloadExceptionTargetPlatform: getNameForTargetPlatform(TargetPlatform.android_arm),
reloadExceptionSdkName: 'Example',
reloadExceptionEmulator: 'false',
reloadExceptionFullRestart: 'false',
})).called(1);
}, overrides: <Type, Generator>{
Usage: () => MockUsage(),
}));
test('ResidentRunner Can handle an RPC exception from hot restart', () => testbed.run(() async {
when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) async {
return 'Example';
});
when(mockDevice.targetPlatform).thenAnswer((Invocation invocation) async {
return TargetPlatform.android_arm;
});
when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) async {
return false;
});
when(mockDevice.supportsHotRestart).thenReturn(true);
final Completer<DebugConnectionInfo> onConnectionInfo = Completer<DebugConnectionInfo>.sync();
final Completer<void> onAppStart = Completer<void>.sync();
unawaited(residentRunner.attach(
appStartedCompleter: onAppStart,
connectionInfoCompleter: onConnectionInfo,
));
await onAppStart.future;
when(mockFlutterDevice.updateDevFS(
mainPath: anyNamed('mainPath'),
target: anyNamed('target'),
bundle: anyNamed('bundle'),
firstBuildTime: anyNamed('firstBuildTime'),
bundleFirstUpload: anyNamed('bundleFirstUpload'),
bundleDirty: anyNamed('bundleDirty'),
fullRestart: anyNamed('fullRestart'),
projectRootPath: anyNamed('projectRootPath'),
pathToReload: anyNamed('pathToReload'),
invalidatedFiles: anyNamed('invalidatedFiles'),
)).thenThrow(RpcException(666, 'something bad happened'));
final OperationResult result = await residentRunner.restart(fullRestart: true);
expect(result.fatal, true);
expect(result.code, 1);
verify(flutterUsage.sendEvent('hot', 'exception', parameters: <String, String>{
reloadExceptionTargetPlatform: getNameForTargetPlatform(TargetPlatform.android_arm),
reloadExceptionSdkName: 'Example',
reloadExceptionEmulator: 'false',
reloadExceptionFullRestart: 'true',
})).called(1);
}, overrides: <Type, Generator>{
Usage: () => MockUsage(),
}));
test('ResidentRunner printHelpDetails', () => testbed.run(() {
when(mockDevice.supportsHotRestart).thenReturn(true);
when(mockDevice.supportsScreenshot).thenReturn(true);
residentRunner.printHelp(details: true);
final BufferLogger bufferLogger = context.get<Logger>();
// supports service protocol
expect(residentRunner.supportsServiceProtocol, true);
expect(bufferLogger.statusText, contains('"w"'));
expect(bufferLogger.statusText, contains('"t"'));
expect(bufferLogger.statusText, contains('"P"'));
expect(bufferLogger.statusText, contains('"a"'));
// isRunningDebug
expect(residentRunner.isRunningDebug, true);
expect(bufferLogger.statusText, contains('"L"'));
expect(bufferLogger.statusText, contains('"S"'));
expect(bufferLogger.statusText, contains('"U"'));
expect(bufferLogger.statusText, contains('"i"'));
expect(bufferLogger.statusText, contains('"p"'));
expect(bufferLogger.statusText, contains('"o"'));
expect(bufferLogger.statusText, contains('"z"'));
// screenshot
expect(bufferLogger.statusText, contains('"s"'));
}));
test('ResidentRunner can take screenshot on debug device', () => testbed.run(() async {
when(mockDevice.supportsScreenshot).thenReturn(true);
when(mockDevice.takeScreenshot(any))
.thenAnswer((Invocation invocation) async { .thenAnswer((Invocation invocation) async {
return testUri; final File file = invocation.positionalArguments.first;
}); file.writeAsBytesSync(List<int>.generate(1024, (int i) => i));
when(mockFlutterDevice.vmServices).thenReturn(<VMService>[
mockVMService,
]);
when(mockFlutterDevice.refreshViews()).thenAnswer((Invocation invocation) async { });
// VMService mocks.
when(mockVMService.wsAddress).thenReturn(testUri);
when(mockVMService.done).thenAnswer((Invocation invocation) {
final Completer<void> result = Completer<void>.sync();
return result.future;
});
when(mockIsolate.resume()).thenAnswer((Invocation invocation) {
return Future<Map<String, Object>>.value(null);
});
when(mockIsolate.flutterExit()).thenAnswer((Invocation invocation) {
return Future<Map<String, Object>>.value(null);
});
}); });
final BufferLogger bufferLogger = context.get<Logger>();
test('Can attach to device successfully', () => testbed.run(() async { await residentRunner.screenshot(mockFlutterDevice);
final Completer<DebugConnectionInfo> onConnectionInfo = Completer<DebugConnectionInfo>.sync();
final Completer<void> onAppStart = Completer<void>.sync();
final Future<int> result = residentRunner.attach(
appStartedCompleter: onAppStart,
connectionInfoCompleter: onConnectionInfo,
);
final Future<DebugConnectionInfo> connectionInfo = onConnectionInfo.future;
expect(await result, 0); // disables debug banner.
verify(mockIsolate.flutterDebugAllowBanner(false)).called(1);
// Enables debug banner.
verify(mockIsolate.flutterDebugAllowBanner(true)).called(1);
expect(bufferLogger.statusText, contains('1kB'));
}));
verify(mockFlutterDevice.initLogReader()).called(1); test('ResidentRunner bails taking screenshot on debug device if debugAllowBanner throws pre', () => testbed.run(() async {
when(mockDevice.supportsScreenshot).thenReturn(true);
when(mockIsolate.flutterDebugAllowBanner(false)).thenThrow(Exception());
final BufferLogger bufferLogger = context.get<Logger>();
expect(onConnectionInfo.isCompleted, true); await residentRunner.screenshot(mockFlutterDevice);
expect((await connectionInfo).baseUri, 'foo://bar');
expect(onAppStart.isCompleted, true);
}));
test('Can handle an RPC exception from hot reload', () => testbed.run(() async { expect(bufferLogger.errorText, contains('Error'));
when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) async { }));
return 'Example';
}); test('ResidentRunner bails taking screenshot on debug device if debugAllowBanner throws post', () => testbed.run(() async {
when(mockDevice.targetPlatform).thenAnswer((Invocation invocation) async { when(mockDevice.supportsScreenshot).thenReturn(true);
return TargetPlatform.android_arm; when(mockIsolate.flutterDebugAllowBanner(true)).thenThrow(Exception());
}); final BufferLogger bufferLogger = context.get<Logger>();
when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) async {
return false; await residentRunner.screenshot(mockFlutterDevice);
});
final Completer<DebugConnectionInfo> onConnectionInfo = Completer<DebugConnectionInfo>.sync(); expect(bufferLogger.errorText, contains('Error'));
final Completer<void> onAppStart = Completer<void>.sync(); }));
unawaited(residentRunner.attach(
appStartedCompleter: onAppStart, test('ResidentRunner bails taking screenshot on debug device if takeScreenshot throws', () => testbed.run(() async {
connectionInfoCompleter: onConnectionInfo, when(mockDevice.supportsScreenshot).thenReturn(true);
)); when(mockDevice.takeScreenshot(any)).thenThrow(Exception());
await onAppStart.future; final BufferLogger bufferLogger = context.get<Logger>();
when(mockFlutterDevice.updateDevFS(
mainPath: anyNamed('mainPath'), await residentRunner.screenshot(mockFlutterDevice);
target: anyNamed('target'),
bundle: anyNamed('bundle'), expect(bufferLogger.errorText, contains('Error'));
firstBuildTime: anyNamed('firstBuildTime'), }));
bundleFirstUpload: anyNamed('bundleFirstUpload'),
bundleDirty: anyNamed('bundleDirty'), test('ResidentRunner can\'t take screenshot on device without support', () => testbed.run(() {
fullRestart: anyNamed('fullRestart'), when(mockDevice.supportsScreenshot).thenReturn(false);
projectRootPath: anyNamed('projectRootPath'),
pathToReload: anyNamed('pathToReload'), expect(() => residentRunner.screenshot(mockFlutterDevice),
invalidatedFiles: anyNamed('invalidatedFiles'), throwsA(isInstanceOf<AssertionError>()));
)).thenThrow(RpcException(666, 'something bad happened')); }));
final OperationResult result = await residentRunner.restart(fullRestart: false); test('ResidentRunner does not toggle banner in non-debug mode', () => testbed.run(() async {
expect(result.fatal, true); residentRunner = HotRunner(
expect(result.code, 1); <FlutterDevice>[
verify(flutterUsage.sendEvent('hot', 'exception', parameters: <String, String>{ mockFlutterDevice,
reloadExceptionTargetPlatform: getNameForTargetPlatform(TargetPlatform.android_arm), ],
reloadExceptionSdkName: 'Example', stayResident: false,
reloadExceptionEmulator: 'false', debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
reloadExceptionFullRestart: 'false', );
})).called(1); when(mockDevice.supportsScreenshot).thenReturn(true);
}, overrides: <Type, Generator>{ when(mockDevice.takeScreenshot(any))
Usage: () => MockUsage(), .thenAnswer((Invocation invocation) async {
})); final File file = invocation.positionalArguments.first;
file.writeAsBytesSync(List<int>.generate(1024, (int i) => i));
test('Can handle an RPC exception from hot restart', () => testbed.run(() async {
when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) async {
return 'Example';
});
when(mockDevice.targetPlatform).thenAnswer((Invocation invocation) async {
return TargetPlatform.android_arm;
});
when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) async {
return false;
});
when(mockDevice.supportsHotRestart).thenReturn(true);
final Completer<DebugConnectionInfo> onConnectionInfo = Completer<DebugConnectionInfo>.sync();
final Completer<void> onAppStart = Completer<void>.sync();
unawaited(residentRunner.attach(
appStartedCompleter: onAppStart,
connectionInfoCompleter: onConnectionInfo,
));
await onAppStart.future;
when(mockFlutterDevice.updateDevFS(
mainPath: anyNamed('mainPath'),
target: anyNamed('target'),
bundle: anyNamed('bundle'),
firstBuildTime: anyNamed('firstBuildTime'),
bundleFirstUpload: anyNamed('bundleFirstUpload'),
bundleDirty: anyNamed('bundleDirty'),
fullRestart: anyNamed('fullRestart'),
projectRootPath: anyNamed('projectRootPath'),
pathToReload: anyNamed('pathToReload'),
invalidatedFiles: anyNamed('invalidatedFiles'),
)).thenThrow(RpcException(666, 'something bad happened'));
final OperationResult result = await residentRunner.restart(fullRestart: true);
expect(result.fatal, true);
expect(result.code, 1);
verify(flutterUsage.sendEvent('hot', 'exception', parameters: <String, String>{
reloadExceptionTargetPlatform: getNameForTargetPlatform(TargetPlatform.android_arm),
reloadExceptionSdkName: 'Example',
reloadExceptionEmulator: 'false',
reloadExceptionFullRestart: 'true',
})).called(1);
}, overrides: <Type, Generator>{
Usage: () => MockUsage(),
}));
group('FlutterDevice' , () {
test('Will not exit a paused isolate', () => testbed.run(() async {
final TestFlutterDevice flutterDevice = TestFlutterDevice(
mockDevice,
<FlutterView>[ mockFlutterView ],
);
final MockServiceEvent mockServiceEvent = MockServiceEvent();
when(mockServiceEvent.isPauseEvent).thenReturn(true);
when(mockIsolate.pauseEvent).thenReturn(mockServiceEvent);
when(mockDevice.supportsFlutterExit).thenReturn(true);
await flutterDevice.exitApps();
verifyNever(mockIsolate.flutterExit());
verify(mockDevice.stopApp(any)).called(1);
}));
test('Will exit an un-paused isolate', () => testbed.run(() async {
final TestFlutterDevice flutterDevice = TestFlutterDevice(
mockDevice,
<FlutterView> [mockFlutterView ],
);
final MockServiceEvent mockServiceEvent = MockServiceEvent();
when(mockServiceEvent.isPauseEvent).thenReturn(false);
when(mockIsolate.pauseEvent).thenReturn(mockServiceEvent);
when(mockDevice.supportsFlutterExit).thenReturn(true);
await flutterDevice.exitApps();
verify(mockIsolate.flutterExit()).called(1);
}));
}); });
}); final BufferLogger bufferLogger = context.get<Logger>();
await residentRunner.screenshot(mockFlutterDevice);
// doesn't disabled debug banner.
verifyNever(mockIsolate.flutterDebugAllowBanner(false));
// doesn't enable debug banner.
verifyNever(mockIsolate.flutterDebugAllowBanner(true));
expect(bufferLogger.statusText, contains('1kB'));
}));
test('FlutterDevice will not exit a paused isolate', () => testbed.run(() async {
final TestFlutterDevice flutterDevice = TestFlutterDevice(
mockDevice,
<FlutterView>[ mockFlutterView ],
);
final MockServiceEvent mockServiceEvent = MockServiceEvent();
when(mockServiceEvent.isPauseEvent).thenReturn(true);
when(mockIsolate.pauseEvent).thenReturn(mockServiceEvent);
when(mockDevice.supportsFlutterExit).thenReturn(true);
await flutterDevice.exitApps();
verifyNever(mockIsolate.flutterExit());
verify(mockDevice.stopApp(any)).called(1);
}));
test('FlutterDevice will exit an un-paused isolate', () => testbed.run(() async {
final TestFlutterDevice flutterDevice = TestFlutterDevice(
mockDevice,
<FlutterView> [mockFlutterView ],
);
final MockServiceEvent mockServiceEvent = MockServiceEvent();
when(mockServiceEvent.isPauseEvent).thenReturn(false);
when(mockIsolate.pauseEvent).thenReturn(mockServiceEvent);
when(mockDevice.supportsFlutterExit).thenReturn(true);
await flutterDevice.exitApps();
verify(mockIsolate.flutterExit()).called(1);
}));
} }
class MockFlutterDevice extends Mock implements FlutterDevice {} class MockFlutterDevice extends Mock implements FlutterDevice {}
......
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