Commit ad19ec7e authored by Justin Cohen's avatar Justin Cohen Committed by Commit Bot

[ios] Add support for safe_mode_egtests in EG2.

Update matchers to not use GREYMatchers anymore
Remove -testInvocations call for iOS 10.2
Add support for shared EarlGreyScopedBlockSwizzler.
Add helper util to call -presentSafeMode.

Bug: 987646
Change-Id: I783e84af095b87f84d2502c14a308dc41a6eef54
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1863950Reviewed-by: default avatarEugene But <eugenebut@chromium.org>
Commit-Queue: Justin Cohen <justincohen@chromium.org>
Auto-Submit: Justin Cohen <justincohen@chromium.org>
Cr-Commit-Position: refs/heads/master@{#707070}
parent 2dd1593f
......@@ -30,12 +30,14 @@ source_set("safe_mode") {
source_set("eg_tests") {
configs += [ "//build/config/compiler:enable_arc" ]
defines = [ "CHROME_EARL_GREY_1" ]
testonly = true
sources = [
"safe_mode_egtest.mm",
]
deps = [
":safe_mode",
":test_support",
"//base",
"//ios/chrome/app:app_internal",
"//ios/chrome/app/strings",
......@@ -44,10 +46,77 @@ source_set("eg_tests") {
"//ios/chrome/test/app:test_support",
"//ios/chrome/test/base",
"//ios/chrome/test/earl_grey:test_support",
"//ios/testing/earl_grey:earl_grey_support",
"//ios/third_party/earl_grey:earl_grey+link",
]
}
source_set("test_support") {
defines = [ "CHROME_EARL_GREY_1" ]
configs += [ "//build/config/compiler:enable_arc" ]
testonly = true
sources = [
"safe_mode_app_interface.h",
"safe_mode_app_interface.mm",
]
deps = [
":safe_mode",
"//ios/chrome/test/app:test_support",
]
}
source_set("eg_app_support+eg2") {
defines = [ "CHROME_EARL_GREY_2" ]
configs += [
"//build/config/compiler:enable_arc",
"//build/config/ios:xctest_config",
]
testonly = true
sources = [
"safe_mode_app_interface.h",
"safe_mode_app_interface.mm",
]
deps = [
":safe_mode",
"//ios/chrome/test/app:test_support",
]
}
source_set("eg_test_support+eg2") {
defines = [ "CHROME_EARL_GREY_2" ]
configs += [
"//build/config/compiler:enable_arc",
"//build/config/ios:xctest_config",
]
testonly = true
sources = [
"safe_mode_app_interface.h",
]
}
source_set("eg2_tests") {
defines = [ "CHROME_EARL_GREY_2" ]
configs += [
"//build/config/compiler:enable_arc",
"//build/config/ios:xctest_config",
]
testonly = true
sources = [
"safe_mode_egtest.mm",
]
deps = [
":eg_test_support+eg2",
"//base:base",
"//ios/chrome/app/strings:ios_chromium_strings_grit",
"//ios/chrome/test/base:base",
"//ios/chrome/test/earl_grey:eg_test_support+eg2",
"//ios/testing/earl_grey:eg_test_support+eg2",
"//ios/third_party/earl_grey2:test_lib",
]
libs = [ "UIKit.framework" ]
}
source_set("unit_tests") {
configs += [ "//build/config/compiler:enable_arc" ]
testonly = true
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef IOS_CHROME_BROWSER_UI_SAFE_MODE_SAFE_MODE_APP_INTERFACE_H_
#define IOS_CHROME_BROWSER_UI_SAFE_MODE_SAFE_MODE_APP_INTERFACE_H_
#import <UIKit/UIKit.h>
// EarlGreyScopedBlockSwizzlerAppInterface contains the app-side
// implementation for helpers. These helpers are compiled into
// the app binary and can be called from either app or test code.
@interface SafeModeAppInterface : NSObject
// Presents the SafeModeViewController UI.
+ (void)presentSafeMode;
@end
#endif // IOS_CHROME_BROWSER_UI_SAFE_MODE_SAFE_MODE_APP_INTERFACE_H_
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ios/chrome/browser/ui/safe_mode/safe_mode_app_interface.h"
#import "ios/chrome/browser/ui/safe_mode/safe_mode_view_controller.h"
#import "ios/chrome/test/app/chrome_test_util.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
@implementation SafeModeAppInterface
+ (void)presentSafeMode {
SafeModeViewController* safeModeController =
[[SafeModeViewController alloc] initWithDelegate:nil];
[chrome_test_util::GetActiveViewController()
presentViewController:safeModeController
animated:NO
completion:nil];
}
@end
......@@ -2,61 +2,61 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import <EarlGrey/EarlGrey.h>
#import <XCTest/XCTest.h>
#include "base/feature_list.h"
#include "base/ios/ios_util.h"
#include "base/logging.h"
#include "base/mac/foundation_util.h"
#import "ios/chrome/browser/ui/safe_mode/safe_mode_view_controller.h"
#include "ios/chrome/browser/ui/util/ui_util.h"
#include "ios/chrome/browser/ui/safe_mode/safe_mode_app_interface.h"
#include "ios/chrome/grit/ios_chromium_strings.h"
#import "ios/chrome/test/app/chrome_test_util.h"
#import "ios/chrome/test/base/scoped_block_swizzler.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey_app_interface.h"
#import "ios/chrome/test/earl_grey/chrome_matchers.h"
#import "ios/chrome/test/earl_grey/chrome_test_case.h"
#import "ios/chrome/test/earl_grey/earl_grey_scoped_block_swizzler.h"
#import "ios/testing/earl_grey/app_launch_manager.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
#if defined(CHROME_EARL_GREY_2)
GREY_STUB_CLASS_IN_APP_MAIN_QUEUE(SafeModeAppInterface)
#endif // defined(CHROME_EARL_GREY_2)
using chrome_test_util::ButtonWithAccessibilityLabel;
namespace {
// Verifies that |message| is displayed.
void AssertMessageOnPage(NSString* message) {
id<GREYMatcher> messageMatcher = [GREYMatchers matcherForText:message];
id<GREYMatcher> messageMatcher =
grey_allOf(grey_text(message), grey_kindOfClass([UILabel class]),
grey_sufficientlyVisible(), nil);
[[EarlGrey selectElementWithMatcher:messageMatcher]
assertWithMatcher:grey_notNil()];
}
// Verifies that |message| is not displayed.
void AssertMessageNotOnPage(NSString* message) {
id<GREYMatcher> messageMatcher = [GREYMatchers matcherForText:message];
id<GREYMatcher> messageMatcher =
grey_allOf(grey_text(message), grey_kindOfClass([UILabel class]),
grey_sufficientlyVisible(), nil);
[[EarlGrey selectElementWithMatcher:messageMatcher]
assertWithMatcher:grey_nil()];
}
// Verifies that the button to reload chrome is displayed.
void AssertTryAgainButtonOnPage() {
NSString* tryAgain =
NSLocalizedString(@"IDS_IOS_SAFE_MODE_RELOAD_CHROME", @"");
// This is uppercased to match MDC button label convention.
NSString* tryAgainPrimaryAction =
[tryAgain uppercaseStringWithLocale:[NSLocale currentLocale]];
id<GREYMatcher> tryAgainMatcher =
[GREYMatchers matcherForButtonTitle:tryAgainPrimaryAction];
id<GREYMatcher> tryAgainMatcher = ButtonWithAccessibilityLabel(
NSLocalizedString(@"IDS_IOS_SAFE_MODE_RELOAD_CHROME", @""));
[[EarlGrey selectElementWithMatcher:tryAgainMatcher]
assertWithMatcher:grey_notNil()];
}
} // namespace
// Expose internal class methods for swizzling.
@interface SafeModeViewController (Testing)
+ (BOOL)detectedThirdPartyMods;
+ (BOOL)hasReportToUpload;
- (NSArray*)startupCrashModules;
@end
// Tests the display of Safe Mode Controller under different error states of
// jailbroken-ness and whether a crash dump was saved.
......@@ -65,33 +65,18 @@ void AssertTryAgainButtonOnPage() {
@implementation SafeModeTestCase
// Per crbug.com/845186, Disable flakey iPad Retina tests that are limited
// to iOS 10.2.
+ (NSArray*)testInvocations {
#if TARGET_IPHONE_SIMULATOR
if ([ChromeEarlGrey isIPadIdiom] && !base::ios::IsRunningOnOrLater(10, 3, 0))
return @[];
#endif // TARGET_IPHONE_SIMULATOR
return [super testInvocations];
}
// Tests that Safe Mode crash upload screen is displayed when there are crash
// reports to upload.
- (void)testSafeModeSendingCrashReport {
// Mocks the +hasReportToUpload method by swizzling to return positively that
// there are crash reports to upload.
ScopedBlockSwizzler hasReport([SafeModeViewController class],
@selector(hasReportToUpload), ^{
return YES;
});
// Instantiates a Safe Mode controller and displays it.
SafeModeViewController* safeModeController =
[[SafeModeViewController alloc] initWithDelegate:nil];
[chrome_test_util::GetActiveViewController()
presentViewController:safeModeController
animated:NO
completion:nil];
// TODO(crbug.com/1015272): Consider moving from swizzling to a delegate.
EarlGreyScopedBlockSwizzler hasReport(@"SafeModeViewController",
@"hasReportToUpload", ^{
return YES;
});
[SafeModeAppInterface presentSafeMode];
// Verifies screen content that shows that crash report is being uploaded.
AssertMessageOnPage(NSLocalizedString(@"IDS_IOS_SAFE_MODE_AW_SNAP", @""));
AssertMessageOnPage(
......@@ -106,23 +91,17 @@ void AssertTryAgainButtonOnPage() {
- (void)testSafeModeDetectedThirdPartyMods {
// Mocks the +detectedThirdPartyMods method by swizzling to return positively
// that device appears to be jailbroken and contains third party mods.
ScopedBlockSwizzler thirdParty([SafeModeViewController class],
@selector(detectedThirdPartyMods), ^{
return YES;
});
// TODO(crbug.com/1015272): Consider moving from swizzling to a delegate.
EarlGreyScopedBlockSwizzler thirdParty(@"SafeModeViewController",
@"detectedThirdPartyMods", ^{
return YES;
});
// Returns an empty list to simulate no known mods detected.
ScopedBlockSwizzler badModules([SafeModeViewController class],
@selector(startupCrashModules), ^{
return @[];
});
// Instantiates a Safe Mode controller and displays it.
SafeModeViewController* safeModeController =
[[SafeModeViewController alloc] initWithDelegate:nil];
[chrome_test_util::GetActiveViewController()
presentViewController:safeModeController
animated:NO
completion:nil];
EarlGreyScopedBlockSwizzler badModules(@"SafeModeViewController",
@"startupCrashModules", ^{
return @[];
});
[SafeModeAppInterface presentSafeMode];
// Verifies screen content that does not show crash report being uploaded.
// When devices are jailbroken, the crash reports are not very useful.
AssertMessageOnPage(NSLocalizedString(@"IDS_IOS_SAFE_MODE_AW_SNAP", @""));
......@@ -139,27 +118,22 @@ void AssertTryAgainButtonOnPage() {
- (void)testSafeModeBothThirdPartyModsAndHasReport {
// Mocks the +detectedThirdPartyMods method by swizzling to return positively
// that device appears to be jailbroken and contains third party mods.
ScopedBlockSwizzler thirdParty([SafeModeViewController class],
@selector(detectedThirdPartyMods), ^{
return YES;
});
// TODO(crbug.com/1015272): Consider moving from swizzling to a delegate.
EarlGreyScopedBlockSwizzler thirdParty(@"SafeModeViewController",
@"detectedThirdPartyMods", ^{
return YES;
});
// Mocked list of bad jailbroken mods. These will be checked later.
NSArray* badModulesList = @[ @"iAmBad", @"MJackson" ];
ScopedBlockSwizzler badModules([SafeModeViewController class],
@selector(startupCrashModules), ^{
return badModulesList;
});
ScopedBlockSwizzler hasReport([SafeModeViewController class],
@selector(hasReportToUpload), ^{
return YES;
});
// Instantiates a Safe Mode controller and displays it.
SafeModeViewController* safeModeController =
[[SafeModeViewController alloc] initWithDelegate:nil];
[chrome_test_util::GetActiveViewController()
presentViewController:safeModeController
animated:NO
completion:nil];
EarlGreyScopedBlockSwizzler badModules(@"SafeModeViewController",
@"startupCrashModules", ^{
return badModulesList;
});
EarlGreyScopedBlockSwizzler hasReport(@"SafeModeViewController",
@"hasReportToUpload", ^{
return YES;
});
[SafeModeAppInterface presentSafeMode];
// Verifies screen content that does not show crash report being uploaded.
// When devices are jailbroken, the crash reports are not very useful.
AssertMessageOnPage(NSLocalizedString(@"IDS_IOS_SAFE_MODE_AW_SNAP", @""));
......
......@@ -212,6 +212,10 @@ source_set("test_support") {
"chrome_test_case.mm",
"chrome_test_case_app_interface.h",
"chrome_test_case_app_interface.mm",
"earl_grey_scoped_block_swizzler.h",
"earl_grey_scoped_block_swizzler.mm",
"earl_grey_scoped_block_swizzler_app_interface.h",
"earl_grey_scoped_block_swizzler_app_interface.mm",
"hardware_keyboard_util.h",
"hardware_keyboard_util.mm",
"scoped_block_popups_pref.h",
......@@ -248,6 +252,8 @@ source_set("test_support") {
"//ios/chrome/browser/ui/payments:payments_ui",
"//ios/chrome/browser/ui/popup_menu:constants",
"//ios/chrome/browser/ui/recent_tabs:recent_tabs_ui_constants",
"//ios/chrome/browser/ui/safe_mode",
"//ios/chrome/browser/ui/safe_mode:test_support",
"//ios/chrome/browser/ui/settings:settings",
"//ios/chrome/browser/ui/settings/autofill",
"//ios/chrome/browser/ui/settings/autofill:feature_flags",
......@@ -267,6 +273,7 @@ source_set("test_support") {
"//ios/chrome/browser/ui/util",
"//ios/chrome/browser/web:tab_id_tab_helper",
"//ios/chrome/test/app:test_support",
"//ios/chrome/test/base",
"//ios/testing:verify_custom_webkit",
"//ios/testing/earl_grey:earl_grey_support",
"//ios/third_party/material_components_ios",
......@@ -329,6 +336,8 @@ source_set("eg_app_support+eg2") {
"chrome_matchers_app_interface.mm",
"chrome_test_case_app_interface.h",
"chrome_test_case_app_interface.mm",
"earl_grey_scoped_block_swizzler_app_interface.h",
"earl_grey_scoped_block_swizzler_app_interface.mm",
"hardware_keyboard_util.h",
"hardware_keyboard_util.mm",
]
......@@ -360,6 +369,8 @@ source_set("eg_app_support+eg2") {
"//ios/chrome/browser/ui/payments:payments_ui",
"//ios/chrome/browser/ui/popup_menu:constants",
"//ios/chrome/browser/ui/recent_tabs:recent_tabs_ui_constants",
"//ios/chrome/browser/ui/safe_mode",
"//ios/chrome/browser/ui/safe_mode:eg_app_support+eg2",
"//ios/chrome/browser/ui/settings:settings",
"//ios/chrome/browser/ui/settings/autofill",
"//ios/chrome/browser/ui/settings/autofill:feature_flags",
......@@ -377,6 +388,7 @@ source_set("eg_app_support+eg2") {
"//ios/chrome/browser/ui/util",
"//ios/chrome/browser/web:tab_id_tab_helper",
"//ios/chrome/test/app:test_support",
"//ios/chrome/test/base",
"//ios/testing:nserror_support",
"//ios/testing:verify_custom_webkit",
"//ios/testing/earl_grey:eg_app_support+eg2",
......@@ -422,6 +434,9 @@ source_set("eg_test_support+eg2") {
"chrome_test_case.h",
"chrome_test_case.mm",
"chrome_test_case_app_interface.h",
"earl_grey_scoped_block_swizzler.h",
"earl_grey_scoped_block_swizzler.mm",
"earl_grey_scoped_block_swizzler_app_interface.h",
"scoped_block_popups_pref.h",
"scoped_block_popups_pref.mm",
]
......@@ -435,6 +450,7 @@ source_set("eg_test_support+eg2") {
"//ios/chrome/app/strings",
"//ios/chrome/browser/ui/popup_menu:constants",
"//ios/chrome/browser/ui/recent_tabs:recent_tabs_ui_constants",
"//ios/chrome/browser/ui/safe_mode:eg_test_support+eg2",
"//ios/chrome/browser/ui/tab_grid:tab_grid_ui_constants",
"//ios/chrome/browser/ui/tab_grid/grid:grid_ui_constants",
"//ios/testing:http_server_bundle_data",
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef IOS_CHROME_TEST_EARL_GREY_EARL_GREY_SCOPED_BLOCK_SWIZZLER_H_
#define IOS_CHROME_TEST_EARL_GREY_EARL_GREY_SCOPED_BLOCK_SWIZZLER_H_
#import <UIKit/UIKit.h>
#include "base/macros.h"
// Helper class that wraps ScopedBlockSwizzler for use in EG1 and EG2 tests.
class EarlGreyScopedBlockSwizzler {
public:
// Constructs a new ScopedBlockSwizzler via the
// EarlGreyScopedBlockSwizzlerAppInterface interface.
EarlGreyScopedBlockSwizzler(NSString* target, NSString* selector, id block);
// Destroys the ScopedBlockSwizzler object via the
// EarlGreyScopedBlockSwizzlerAppInterface interface.
virtual ~EarlGreyScopedBlockSwizzler();
private:
// id used to track creation and destruction of swizzled block.
int unique_id_ = 0;
DISALLOW_COPY_AND_ASSIGN(EarlGreyScopedBlockSwizzler);
};
#endif // IOS_CHROME_TEST_EARL_GREY_EARL_GREY_SCOPED_BLOCK_SWIZZLER_H_
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ios/chrome/test/earl_grey/earl_grey_scoped_block_swizzler.h"
#include "ios/chrome/test/earl_grey/earl_grey_scoped_block_swizzler_app_interface.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
#if defined(CHROME_EARL_GREY_2)
GREY_STUB_CLASS_IN_APP_MAIN_QUEUE(EarlGreyScopedBlockSwizzlerAppInterface)
#endif // defined(CHROME_EARL_GREY_2)
EarlGreyScopedBlockSwizzler::EarlGreyScopedBlockSwizzler(NSString* target,
NSString* selector,
id block)
: unique_id_([EarlGreyScopedBlockSwizzlerAppInterface
createScopedBlockSwizzlerForTarget:target
withSelector:selector
withBlock:block]) {}
EarlGreyScopedBlockSwizzler::~EarlGreyScopedBlockSwizzler() {
[EarlGreyScopedBlockSwizzlerAppInterface
deleteScopedBlockSwizzlerForID:unique_id_];
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef IOS_CHROME_TEST_EARL_GREY_EARL_GREY_SCOPED_BLOCK_SWIZZLER_APP_INTERFACE_H_
#define IOS_CHROME_TEST_EARL_GREY_EARL_GREY_SCOPED_BLOCK_SWIZZLER_APP_INTERFACE_H_
#import <UIKit/UIKit.h>
// EarlGreyScopedBlockSwizzlerAppInterface contains the app-side
// implementation for helpers. These helpers are compiled into
// the app binary and can be called from either app or test code.
@interface EarlGreyScopedBlockSwizzlerAppInterface : NSObject
// Creates and retains a ScopedBlockSwizzler. Returns a unique id to be used
// to delete that ScopedBlockSwizzler.
+ (int)createScopedBlockSwizzlerForTarget:(NSString*)target
withSelector:(NSString*)selector
withBlock:(id)block;
// Deletes a ScopedBlockSwizzler based on it's unique id.
+ (void)deleteScopedBlockSwizzlerForID:(int)uniqueID;
@end
#endif // IOS_CHROME_TEST_EARL_GREY_EARL_GREY_SCOPED_BLOCK_SWIZZLER_APP_INTERFACE_H_
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ios/chrome/test/earl_grey/earl_grey_scoped_block_swizzler_app_interface.h"
#include <map>
#include "base/logging.h"
#include "ios/chrome/test/base/scoped_block_swizzler.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
@interface EarlGreyScopedBlockSwizzlerHelper : NSObject {
// Unique IDs used with an EG2 safe basic type that can be used to later
// delete the ScopedBlockSwizzler.
int _swizzledIDs;
// Map of ScopedBlockSwizzler-ed objects, with a tracking int.
std::map<int, std::unique_ptr<ScopedBlockSwizzler>> _map;
}
// Inserts and removes from |map|.
- (int)insertScopedBlockSwizzler:(std::unique_ptr<ScopedBlockSwizzler>)swizzler;
- (void)removeScopedBlockSwizzler:(int)uniqueID;
@end
@implementation EarlGreyScopedBlockSwizzlerHelper
- (instancetype)init {
if ((self = [super init])) {
_swizzledIDs = 0;
}
return self;
}
+ (instancetype)sharedInstance {
static EarlGreyScopedBlockSwizzlerHelper* instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[EarlGreyScopedBlockSwizzlerHelper alloc] init];
});
return instance;
}
- (int)insertScopedBlockSwizzler:
(std::unique_ptr<ScopedBlockSwizzler>)swizzler {
_map[++_swizzledIDs] = std::move(swizzler);
return _swizzledIDs;
}
- (void)removeScopedBlockSwizzler:(int)uniqueID {
DCHECK(_map[uniqueID]);
_map.erase(uniqueID);
}
@end
@implementation EarlGreyScopedBlockSwizzlerAppInterface
+ (int)createScopedBlockSwizzlerForTarget:(NSString*)targetString
withSelector:(NSString*)selectorString
withBlock:(id)block {
Class target = NSClassFromString(targetString);
SEL selector = NSSelectorFromString(selectorString);
auto helper = [EarlGreyScopedBlockSwizzlerHelper sharedInstance];
auto swizzler =
std::make_unique<ScopedBlockSwizzler>(target, selector, block);
return [helper insertScopedBlockSwizzler:std::move(swizzler)];
}
+ (void)deleteScopedBlockSwizzlerForID:(int)uniqueID {
auto helper = [EarlGreyScopedBlockSwizzlerHelper sharedInstance];
[helper removeScopedBlockSwizzler:uniqueID];
}
@end
......@@ -57,6 +57,7 @@ chrome_ios_eg2_test("ios_chrome_ui_eg2tests_module") {
"//ios/chrome/browser/ui/open_in:eg2_tests",
"//ios/chrome/browser/ui/page_info:eg2_tests",
"//ios/chrome/browser/ui/sad_tab:eg2_tests",
"//ios/chrome/browser/ui/safe_mode:eg2_tests",
"//ios/chrome/browser/ui/settings/autofill:eg2_tests",
"//ios/chrome/browser/ui/side_swipe:eg2_tests",
"//ios/chrome/browser/ui/tabs:eg2_tests",
......
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