Skip to content

Commit

Permalink
[Mac] Add interactive App Shim test.
Browse files Browse the repository at this point in the history
This test creates shims in the user data dir and actually starts them up and
expects them to connect to the IPC socket.

BUG=168080

Committed: https://src.chromium.org/viewvc/chrome?view=rev&revision=276368

Review URL: https://codereview.chromium.org/316493002

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@277743 0039d316-1c4b-4281-b951-d872f2087c98
  • Loading branch information
jackhou@chromium.org committed Jun 17, 2014
1 parent 3d24645 commit bf57960
Show file tree
Hide file tree
Showing 8 changed files with 328 additions and 5 deletions.
300 changes: 300 additions & 0 deletions apps/app_shim/app_shim_interactive_uitest_mac.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
// Copyright 2014 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.

#import <Cocoa/Cocoa.h>
#include <vector>

#include "apps/app_shim/app_shim_handler_mac.h"
#include "apps/app_shim/app_shim_host_manager_mac.h"
#include "apps/app_shim/extension_app_shim_handler_mac.h"
#include "apps/switches.h"
#include "apps/ui/native_app_window.h"
#include "base/auto_reset.h"
#include "base/callback.h"
#include "base/files/file_path_watcher.h"
#include "base/mac/foundation_util.h"
#include "base/mac/launch_services_util.h"
#include "base/mac/scoped_nsobject.h"
#include "base/path_service.h"
#include "base/process/launch.h"
#include "base/strings/sys_string_conversions.h"
#include "base/test/test_timeouts.h"
#include "chrome/browser/apps/app_browsertest_util.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/extension_test_message_listener.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/web_app_mac.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/mac/app_mode_common.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/extension_registry.h"
#import "ui/events/test/cocoa_test_event_utils.h"

namespace {

// General end-to-end test for app shims.
class AppShimInteractiveTest : public extensions::PlatformAppBrowserTest {
protected:
AppShimInteractiveTest()
: auto_reset_(&g_app_shims_allow_update_and_launch_in_tests, true) {}

private:
// Temporarily enable app shims.
base::AutoReset<bool> auto_reset_;

DISALLOW_COPY_AND_ASSIGN(AppShimInteractiveTest);
};

// Watches for changes to a file. This is designed to be used from the the UI
// thread.
class WindowedFilePathWatcher
: public base::RefCountedThreadSafe<WindowedFilePathWatcher> {
public:
WindowedFilePathWatcher(const base::FilePath& path) : observed_(false) {
content::BrowserThread::PostTask(
content::BrowserThread::FILE,
FROM_HERE,
base::Bind(&WindowedFilePathWatcher::Watch, this, path));
}

void Wait() {
if (observed_)
return;

run_loop_.reset(new base::RunLoop);
run_loop_->Run();
}

protected:
friend class base::RefCountedThreadSafe<WindowedFilePathWatcher>;
virtual ~WindowedFilePathWatcher() {}

void Watch(const base::FilePath& path) {
watcher_.Watch(
path, false, base::Bind(&WindowedFilePathWatcher::Observe, this));
}

void Observe(const base::FilePath& path, bool error) {
content::BrowserThread::PostTask(
content::BrowserThread::UI,
FROM_HERE,
base::Bind(&WindowedFilePathWatcher::StopRunLoop, this));
}

void StopRunLoop() {
observed_ = true;
if (run_loop_.get())
run_loop_->Quit();
}

private:
base::FilePathWatcher watcher_;
bool observed_;
scoped_ptr<base::RunLoop> run_loop_;

DISALLOW_COPY_AND_ASSIGN(WindowedFilePathWatcher);
};

// Watches for an app shim to connect.
class WindowedAppShimLaunchObserver : public apps::AppShimHandler {
public:
WindowedAppShimLaunchObserver(const std::string& app_id)
: app_mode_id_(app_id),
observed_(false) {
apps::AppShimHandler::RegisterHandler(app_id, this);
}

void Wait() {
if (observed_)
return;

run_loop_.reset(new base::RunLoop);
run_loop_->Run();
}

// AppShimHandler overrides:
virtual void OnShimLaunch(Host* host,
apps::AppShimLaunchType launch_type,
const std::vector<base::FilePath>& files) OVERRIDE {
// Remove self and pass through to the default handler.
apps::AppShimHandler::RemoveHandler(app_mode_id_);
apps::AppShimHandler::GetForAppMode(app_mode_id_)
->OnShimLaunch(host, launch_type, files);
observed_ = true;
if (run_loop_.get())
run_loop_->Quit();
}
virtual void OnShimClose(Host* host) OVERRIDE {}
virtual void OnShimFocus(Host* host,
apps::AppShimFocusType focus_type,
const std::vector<base::FilePath>& files) OVERRIDE {}
virtual void OnShimSetHidden(Host* host, bool hidden) OVERRIDE {}
virtual void OnShimQuit(Host* host) OVERRIDE {}

private:
std::string app_mode_id_;
bool observed_;
scoped_ptr<base::RunLoop> run_loop_;

DISALLOW_COPY_AND_ASSIGN(WindowedAppShimLaunchObserver);
};

NSString* GetBundleID(const base::FilePath& shim_path) {
base::FilePath plist_path = shim_path.Append("Contents").Append("Info.plist");
NSMutableDictionary* plist = [NSMutableDictionary
dictionaryWithContentsOfFile:base::mac::FilePathToNSString(plist_path)];
return [plist objectForKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)];
}

bool HasAppShimHost(Profile* profile, const std::string& app_id) {
return g_browser_process->platform_part()
->app_shim_host_manager()
->extension_app_shim_handler()
->FindHost(profile, app_id);
}

} // namespace

// Watches for NSNotifications from the shared workspace.
@interface WindowedNSNotificationObserver : NSObject {
@private
base::scoped_nsobject<NSString> bundleId_;
BOOL notificationReceived_;
scoped_ptr<base::RunLoop> runLoop_;
}

- (id)initForNotification:(NSString*)name
andBundleId:(NSString*)bundleId;
- (void)observe:(NSNotification*)notification;
- (void)wait;
@end

@implementation WindowedNSNotificationObserver

- (id)initForNotification:(NSString*)name
andBundleId:(NSString*)bundleId {
if (self = [super init]) {
bundleId_.reset([[bundleId copy] retain]);
[[[NSWorkspace sharedWorkspace] notificationCenter]
addObserver:self
selector:@selector(observe:)
name:name
object:nil];
}
return self;
}

- (void)observe:(NSNotification*)notification {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

NSRunningApplication* application =
[[notification userInfo] objectForKey:NSWorkspaceApplicationKey];
if (![[application bundleIdentifier] isEqualToString:bundleId_])
return;

[[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
notificationReceived_ = YES;
if (runLoop_.get())
runLoop_->Quit();
}

- (void)wait {
if (notificationReceived_)
return;

runLoop_.reset(new base::RunLoop);
runLoop_->Run();
}

@end

namespace apps {

// Test that launching the shim for an app starts the app, and vice versa.
// These two cases are combined because the time to run the test is dominated
// by loading the extension and creating the shim.
IN_PROC_BROWSER_TEST_F(AppShimInteractiveTest, Launch) {
// Install the app.
const extensions::Extension* app = InstallPlatformApp("minimal");

// Use a WebAppShortcutCreator to get the path.
web_app::WebAppShortcutCreator shortcut_creator(
web_app::GetWebAppDataDirectory(profile()->GetPath(), app->id(), GURL()),
web_app::ShortcutInfoForExtensionAndProfile(app, profile()),
extensions::FileHandlersInfo());
base::FilePath shim_path = shortcut_creator.GetInternalShortcutPath();
EXPECT_FALSE(base::PathExists(shim_path));

// Create the internal app shim by simulating an app update. FilePathWatcher
// is used to wait for file operations on the shim to be finished before
// attempting to launch it. Since all of the file operations are done in the
// same event on the FILE thread, everything will be done by the time the
// watcher's callback is executed.
scoped_refptr<WindowedFilePathWatcher> file_watcher =
new WindowedFilePathWatcher(shim_path);
web_app::UpdateAllShortcuts(base::string16(), profile(), app);
file_watcher->Wait();
NSString* bundle_id = GetBundleID(shim_path);

// Case 1: Launch the shim, it should start the app.
{
ExtensionTestMessageListener launched_listener("Launched", false);
CommandLine shim_cmdline(CommandLine::NO_PROGRAM);
shim_cmdline.AppendSwitch(app_mode::kLaunchedForTest);
ProcessSerialNumber shim_psn;
ASSERT_TRUE(base::mac::OpenApplicationWithPath(
shim_path, shim_cmdline, kLSLaunchDefaults, &shim_psn));
ASSERT_TRUE(launched_listener.WaitUntilSatisfied());

ASSERT_TRUE(GetFirstAppWindow());
EXPECT_TRUE(HasAppShimHost(profile(), app->id()));

// If the window is closed, the shim should quit.
pid_t shim_pid;
EXPECT_EQ(noErr, GetProcessPID(&shim_psn, &shim_pid));
GetFirstAppWindow()->GetBaseWindow()->Close();
ASSERT_TRUE(
base::WaitForSingleProcess(shim_pid, TestTimeouts::action_timeout()));

EXPECT_FALSE(GetFirstAppWindow());
EXPECT_FALSE(HasAppShimHost(profile(), app->id()));
}

// Case 2: Launch the app, it should start the shim.
{
base::scoped_nsobject<WindowedNSNotificationObserver> ns_observer;
ns_observer.reset([[WindowedNSNotificationObserver alloc]
initForNotification:NSWorkspaceDidLaunchApplicationNotification
andBundleId:bundle_id]);
WindowedAppShimLaunchObserver observer(app->id());
LaunchPlatformApp(app);
[ns_observer wait];
observer.Wait();

EXPECT_TRUE(GetFirstAppWindow());
EXPECT_TRUE(HasAppShimHost(profile(), app->id()));

// Quitting the shim will eventually cause it to quit. It actually
// intercepts the -terminate, sends an AppShimHostMsg_QuitApp to Chrome,
// and returns NSTerminateLater. Chrome responds by closing all windows of
// the app. Once all windows are closed, Chrome closes the IPC channel,
// which causes the shim to actually terminate.
NSArray* running_shim = [NSRunningApplication
runningApplicationsWithBundleIdentifier:bundle_id];
ASSERT_EQ(1u, [running_shim count]);

ns_observer.reset([[WindowedNSNotificationObserver alloc]
initForNotification:NSWorkspaceDidTerminateApplicationNotification
andBundleId:bundle_id]);
[base::mac::ObjCCastStrict<NSRunningApplication>(
[running_shim objectAtIndex:0]) terminate];
[ns_observer wait];

EXPECT_FALSE(GetFirstAppWindow());
EXPECT_FALSE(HasAppShimHost(profile(), app->id()));
}
}

} // namespace apps
6 changes: 5 additions & 1 deletion apps/app_shim/chrome_main_app_mode_mac.mm
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,11 @@ int ChromeAppModeStart(const app_mode::ChromeAppModeInfo* info) {
main_message_loop.set_thread_name("MainThread");
base::PlatformThread::SetName("CrAppShimMain");

if (pid == -1) {
// In tests, launching Chrome does nothing, and we won't get a ping response,
// so just assume the socket exists.
if (pid == -1 &&
!CommandLine::ForCurrentProcess()->HasSwitch(
app_mode::kLaunchedForTest)) {
// Launch Chrome if it isn't already running.
ProcessSerialNumber psn;
CommandLine command_line(CommandLine::NO_PROGRAM);
Expand Down
2 changes: 0 additions & 2 deletions chrome/app/app_mode-Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@
<string>@APP_MODE_SHORTCUT_URL@</string>
<key>CrBundleIdentifier</key>
<string>@APP_MODE_BROWSER_BUNDLE_ID@</string>
<key>LSFileQuarantineEnabled</key>
<true/>
<key>LSMinimumSystemVersion</key>
<string>${MACOSX_DEPLOYMENT_TARGET}.0</string>
<key>NSAppleScriptEnabled</key>
Expand Down
5 changes: 5 additions & 0 deletions chrome/browser/web_applications/web_app_mac.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
#include "chrome/browser/web_applications/web_app.h"
#include "extensions/common/manifest_handlers/file_handler_info.h"

// Whether to enable update and launch of app shims in tests. (Normally shims
// are never created or launched in tests). Note that update only creates
// internal shim bundles, i.e. it does not create new shims in ~/Applications.
extern bool g_app_shims_allow_update_and_launch_in_tests;

namespace web_app {

// Returns the full path of the .app shim that would be created by
Expand Down
14 changes: 12 additions & 2 deletions chrome/browser/web_applications/web_app_mac.mm
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/image/image_family.h"

bool g_app_shims_allow_update_and_launch_in_tests = false;

namespace {

// Launch Services Key to run as an agent app, which doesn't launch in the dock.
Expand Down Expand Up @@ -564,7 +566,11 @@ void UpdateFileTypes(NSMutableDictionary* plist,
return succeeded;
}

// Remove the quarantine attribute from both the bundle and the executable.
base::mac::RemoveQuarantineAttribute(dst_path.Append(app_name));
base::mac::RemoveQuarantineAttribute(
dst_path.Append(app_name)
.Append("Contents").Append("MacOS").Append("app_mode_loader"));
++succeeded;
}

Expand Down Expand Up @@ -863,8 +869,10 @@ WebAppShortcutCreator shortcut_creator(
}

void MaybeLaunchShortcut(const ShortcutInfo& shortcut_info) {
if (AppShimsDisabledForTest())
if (AppShimsDisabledForTest() &&
!g_app_shims_allow_update_and_launch_in_tests) {
return;
}

content::BrowserThread::PostTask(
content::BrowserThread::FILE, FROM_HERE,
Expand Down Expand Up @@ -976,8 +984,10 @@ void UpdatePlatformShortcuts(
const ShortcutInfo& shortcut_info,
const extensions::FileHandlersInfo& file_handlers_info) {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
if (AppShimsDisabledForTest())
if (AppShimsDisabledForTest() &&
!g_app_shims_allow_update_and_launch_in_tests) {
return;
}

WebAppShortcutCreator shortcut_creator(
app_data_path, shortcut_info, file_handlers_info);
Expand Down
1 change: 1 addition & 0 deletions chrome/chrome_tests.gypi
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
'HAS_OUT_OF_PROC_TEST_RUNNER',
],
'sources': [
'../apps/app_shim/app_shim_interactive_uitest_mac.mm',
'../apps/app_shim/app_shim_quit_interactive_uitest_mac.mm',
'../apps/app_window_interactive_uitest.cc',
'../ui/base/clipboard/clipboard_unittest.cc',
Expand Down
4 changes: 4 additions & 0 deletions chrome/common/mac/app_mode_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ extern const char kAppListModeId[];
// launch_now = false. This associates the shim without launching the app.
extern const char kLaunchedByChromeProcessId[];

// Indicates to the shim that it was launched for a test, so don't attempt to
// launch Chrome.
extern const char kLaunchedForTest[];

// Path to an app shim bundle. Indicates to Chrome that this shim attempted to
// launch but failed.
extern const char kAppShimError[];
Expand Down
Loading

0 comments on commit bf57960

Please sign in to comment.