Diff
Modified: trunk/Source/WebKit/ChangeLog (260329 => 260330)
--- trunk/Source/WebKit/ChangeLog 2020-04-18 23:32:19 UTC (rev 260329)
+++ trunk/Source/WebKit/ChangeLog 2020-04-19 02:08:52 UTC (rev 260330)
@@ -1,3 +1,62 @@
+2020-04-18 Brady Eidson <beid...@apple.com>
+
+ Fix WebUserContentControllerProxy vs ContentWorld lifetime
+ https://bugs.webkit.org/show_bug.cgi?id=210700
+
+ Reviewed by Alex Christensen.
+
+ Covered by API test.
+
+ WebUserContentControllerProxy currently keeps all of its associated API::ContentWorlds alive via RefPtrs.
+
+ This is despite the fact that all of the associated WebScriptMessageHandlers, UserScripts, and
+ UserStyleSheets already keep their associated API::ContentWorlds alive.
+
+ It then decideds to tell WebProcesses to forget a content world after all of its clients are removed.
+
+ Unfortunately, content worlds are used for more than just content controller stuff. They're used for direct
+ _javascript_ evaluation as well.
+
+ So a client could:
+ - Add a script message handler in a content world.
+ - Evaluate _javascript_ in that content world, setting up some persistent state.
+ - Remove the script message handler.
+ - Find that their persistent state from the _javascript_ evaluation is gone from that world, even though they
+ still retain a usable handle to that world.
+
+ The only party who has any business managing the lifetime of an API::ContentWorld is the API::ContentWorld itself.
+
+ Making this change is:
+ 1 - Nice cleanup
+ 2 - Fixes the above mentioned bug
+
+ * UIProcess/API/APIContentWorld.cpp:
+ (API::ContentWorld::worldForIdentifier):
+ (API::ContentWorld::ContentWorld):
+ (API::ContentWorld::sharedWorldWithName):
+ (API::ContentWorld::~ContentWorld):
+ (API::ContentWorld::addAssociatedUserContentControllerProxy):
+ (API::ContentWorld::userContentControllerProxyDestroyed):
+ * UIProcess/API/APIContentWorld.h:
+
+ * UIProcess/UserContent/WebUserContentControllerProxy.cpp:
+ (WebKit::WebUserContentControllerProxy::parameters const):
+ (WebKit::WebUserContentControllerProxy::addContentWorld):
+ (WebKit::WebUserContentControllerProxy::contentWorldDestroyed):
+ (WebKit::WebUserContentControllerProxy::addUserScript):
+ (WebKit::WebUserContentControllerProxy::removeUserScript):
+ (WebKit::WebUserContentControllerProxy::removeAllUserScripts):
+ (WebKit::WebUserContentControllerProxy::addUserStyleSheet):
+ (WebKit::WebUserContentControllerProxy::removeUserStyleSheet):
+ (WebKit::WebUserContentControllerProxy::removeAllUserStyleSheets):
+ (WebKit::WebUserContentControllerProxy::addUserScriptMessageHandler):
+ (WebKit::WebUserContentControllerProxy::removeUserMessageHandlerForName):
+ (WebKit::WebUserContentControllerProxy::removeAllUserMessageHandlers):
+ (WebKit::WebUserContentControllerProxy::addContentWorldUse): Deleted.
+ (WebKit::WebUserContentControllerProxy::shouldSendRemoveContentWorldsMessage): Deleted.
+ (WebKit::WebUserContentControllerProxy::removeContentWorldUses): Deleted.
+ * UIProcess/UserContent/WebUserContentControllerProxy.h:
+
2020-04-18 David Kilzer <ddkil...@apple.com>
Attempt #4 to fix tvOS build
Modified: trunk/Source/WebKit/UIProcess/API/APIContentWorld.cpp (260329 => 260330)
--- trunk/Source/WebKit/UIProcess/API/APIContentWorld.cpp 2020-04-18 23:32:19 UTC (rev 260329)
+++ trunk/Source/WebKit/UIProcess/API/APIContentWorld.cpp 2020-04-19 02:08:52 UTC (rev 260330)
@@ -27,11 +27,29 @@
#include "APIContentWorld.h"
#include "ContentWorldShared.h"
+#include "WebUserContentControllerProxy.h"
#include <wtf/HashMap.h>
#include <wtf/text/StringHash.h>
namespace API {
+static HashMap<WTF::String, ContentWorld*>& sharedWorldNameMap()
+{
+ static NeverDestroyed<HashMap<WTF::String, ContentWorld*>> sharedMap;
+ return sharedMap;
+}
+
+static HashMap<WebKit::ContentWorldIdentifier, ContentWorld*>& sharedWorldIdentifierMap()
+{
+ static NeverDestroyed<HashMap<WebKit::ContentWorldIdentifier, ContentWorld*>> sharedMap;
+ return sharedMap;
+}
+
+ContentWorld* ContentWorld::worldForIdentifier(WebKit::ContentWorldIdentifier identifer)
+{
+ return sharedWorldIdentifierMap().get(identifer);
+}
+
ContentWorld::ContentWorld(const WTF::String& name)
: m_name(name)
{
@@ -44,17 +62,19 @@
});
m_identifier = WebKit::ContentWorldIdentifier::generate();
+ auto addResult = sharedWorldIdentifierMap().add(m_identifier, this);
+ ASSERT_UNUSED(addResult, addResult.isNewEntry);
}
-static HashMap<WTF::String, ContentWorld*>& sharedWorldMap()
+ContentWorld::ContentWorld(WebKit::ContentWorldIdentifier identifier)
+ : m_identifier(identifier)
{
- static HashMap<WTF::String, ContentWorld*>* sharedMap = new HashMap<WTF::String, ContentWorld*>;
- return *sharedMap;
+ ASSERT(m_identifier == WebKit::pageContentWorldIdentifier());
}
Ref<ContentWorld> ContentWorld::sharedWorldWithName(const WTF::String& name)
{
- auto result = sharedWorldMap().add(name, nullptr);
+ auto result = sharedWorldNameMap().add(name, nullptr);
if (result.isNewEntry) {
result.iterator->value = new ContentWorld(name);
return adoptRef(*result.iterator->value);
@@ -77,11 +97,30 @@
ContentWorld::~ContentWorld()
{
- if (name().isNull())
- return;
+ ASSERT(m_identifier != WebKit::pageContentWorldIdentifier());
- auto taken = sharedWorldMap().take(name());
- ASSERT_UNUSED(taken, taken == this);
+ auto result = sharedWorldIdentifierMap().take(m_identifier);
+ ASSERT_UNUSED(result, result == this || m_identifier == WebKit::pageContentWorldIdentifier());
+
+ if (!name().isNull()) {
+ auto taken = sharedWorldNameMap().take(name());
+ ASSERT_UNUSED(taken, taken == this);
+ }
+
+ for (auto proxy : m_associatedContentControllerProxies)
+ proxy->contentWorldDestroyed(*this);
}
+void ContentWorld::addAssociatedUserContentControllerProxy(WebKit::WebUserContentControllerProxy& proxy)
+{
+ auto addResult = m_associatedContentControllerProxies.add(&proxy);
+ ASSERT_UNUSED(addResult, addResult.isNewEntry);
+}
+
+void ContentWorld::userContentControllerProxyDestroyed(WebKit::WebUserContentControllerProxy& proxy)
+{
+ bool removed = m_associatedContentControllerProxies.remove(&proxy);
+ ASSERT_UNUSED(removed, removed);
+}
+
} // namespace API
Modified: trunk/Source/WebKit/UIProcess/API/APIContentWorld.h (260329 => 260330)
--- trunk/Source/WebKit/UIProcess/API/APIContentWorld.h 2020-04-18 23:32:19 UTC (rev 260329)
+++ trunk/Source/WebKit/UIProcess/API/APIContentWorld.h 2020-04-19 02:08:52 UTC (rev 260330)
@@ -27,12 +27,18 @@
#include "APIObject.h"
#include "ContentWorldShared.h"
+#include <wtf/HashSet.h>
#include <wtf/text/WTFString.h>
+namespace WebKit {
+class WebUserContentControllerProxy;
+}
+
namespace API {
class ContentWorld final : public API::ObjectImpl<API::Object::Type::ContentWorld> {
public:
+ static ContentWorld* worldForIdentifier(WebKit::ContentWorldIdentifier);
static Ref<ContentWorld> sharedWorldWithName(const WTF::String&);
static ContentWorld& pageContentWorld();
static ContentWorld& defaultClientWorld();
@@ -43,13 +49,16 @@
const WTF::String& name() const { return m_name; }
std::pair<WebKit::ContentWorldIdentifier, WTF::String> worldData() const { return { m_identifier, m_name }; }
+ void addAssociatedUserContentControllerProxy(WebKit::WebUserContentControllerProxy&);
+ void userContentControllerProxyDestroyed(WebKit::WebUserContentControllerProxy&);
+
private:
explicit ContentWorld(const WTF::String&);
- explicit ContentWorld(WebKit::ContentWorldIdentifier identifier)
- : m_identifier(identifier) { }
+ explicit ContentWorld(WebKit::ContentWorldIdentifier);
WebKit::ContentWorldIdentifier m_identifier;
WTF::String m_name;
+ HashSet<WebKit::WebUserContentControllerProxy*> m_associatedContentControllerProxies;
};
} // namespace API
Modified: trunk/Source/WebKit/UIProcess/UserContent/WebUserContentControllerProxy.cpp (260329 => 260330)
--- trunk/Source/WebKit/UIProcess/UserContent/WebUserContentControllerProxy.cpp 2020-04-18 23:32:19 UTC (rev 260329)
+++ trunk/Source/WebKit/UIProcess/UserContent/WebUserContentControllerProxy.cpp 2020-04-19 02:08:52 UTC (rev 260330)
@@ -73,6 +73,12 @@
WebUserContentControllerProxy::~WebUserContentControllerProxy()
{
+ for (const auto& identifier : m_associatedContentWorlds) {
+ auto* world = API::ContentWorld::worldForIdentifier(identifier);
+ RELEASE_ASSERT(world);
+ world->userContentControllerProxyDestroyed(*this);
+ }
+
webUserContentControllerProxies().remove(m_identifier);
for (auto& process : m_processes) {
process.removeMessageReceiver(Messages::WebUserContentControllerProxy::messageReceiverName(), identifier());
@@ -110,8 +116,12 @@
parameters.identifier = identifier();
- for (const auto& world : m_contentWorlds)
- parameters.userContentWorlds.append(world.key->worldData());
+ ASSERT(parameters.userContentWorlds.isEmpty());
+ for (const auto& identifier : m_associatedContentWorlds) {
+ auto* world = API::ContentWorld::worldForIdentifier(identifier);
+ RELEASE_ASSERT(world);
+ parameters.userContentWorlds.append(world->worldData());
+ }
for (auto userScript : m_userScripts->elementsOfType<API::UserScript>())
parameters.userScripts.append({ userScript->identifier(), userScript->contentWorld().identifier(), userScript->userScript() });
@@ -149,52 +159,27 @@
webProcessProxy.removeMessageReceiver(Messages::WebUserContentControllerProxy::messageReceiverName(), identifier());
}
-void WebUserContentControllerProxy::addContentWorldUse(API::ContentWorld& world)
+void WebUserContentControllerProxy::addContentWorld(API::ContentWorld& world)
{
- if (&world == &API::ContentWorld::pageContentWorld())
+ if (world.identifier() == pageContentWorldIdentifier())
return;
- auto addResult = m_contentWorlds.add(&world);
- if (addResult.isNewEntry) {
- for (auto& process : m_processes)
- process.send(Messages::WebUserContentController::AddContentWorlds({ world.worldData() }), identifier());
- }
-}
+ auto addResult = m_associatedContentWorlds.add(world.identifier());
+ if (!addResult.isNewEntry)
+ return;
-bool WebUserContentControllerProxy::shouldSendRemoveContentWorldsMessage(API::ContentWorld& world, unsigned numberOfUsesToRemove)
-{
- if (&world == &API::ContentWorld::pageContentWorld())
- return false;
-
- auto it = m_contentWorlds.find(&world);
- for (unsigned i = 0; i < numberOfUsesToRemove; ++i) {
- if (m_contentWorlds.remove(it)) {
- ASSERT(i == (numberOfUsesToRemove - 1));
- return true;
- }
- }
-
- return false;
+ world.addAssociatedUserContentControllerProxy(*this);
+ for (auto& process : m_processes)
+ process.send(Messages::WebUserContentController::AddContentWorlds({ world.worldData() }), identifier());
}
-void WebUserContentControllerProxy::removeContentWorldUses(API::ContentWorld& world, unsigned numberOfUsesToRemove)
+void WebUserContentControllerProxy::contentWorldDestroyed(API::ContentWorld& world)
{
- if (shouldSendRemoveContentWorldsMessage(world, numberOfUsesToRemove)) {
- for (auto& process : m_processes)
- process.send(Messages::WebUserContentController::RemoveContentWorlds({ world.identifier() }), identifier());
- }
-}
+ bool result = m_associatedContentWorlds.remove(world.identifier());
+ ASSERT_UNUSED(result, result);
-void WebUserContentControllerProxy::removeContentWorldUses(HashCountedSet<RefPtr<API::ContentWorld>>& worlds)
-{
- Vector<ContentWorldIdentifier> worldsToRemove;
- for (auto& worldUsePair : worlds) {
- if (shouldSendRemoveContentWorldsMessage(*worldUsePair.key.get(), worldUsePair.value))
- worldsToRemove.append(worldUsePair.key->identifier());
- }
-
for (auto& process : m_processes)
- process.send(Messages::WebUserContentController::RemoveContentWorlds(worldsToRemove), identifier());
+ process.send(Messages::WebUserContentController::RemoveContentWorlds({ world.identifier() }), identifier());
}
void WebUserContentControllerProxy::addUserScript(API::UserScript& userScript, InjectUserScriptImmediately immediately)
@@ -201,7 +186,7 @@
{
Ref<API::ContentWorld> world = userScript.contentWorld();
- addContentWorldUse(world.get());
+ addContentWorld(world.get());
m_userScripts->elements().append(&userScript);
@@ -217,8 +202,6 @@
process.send(Messages::WebUserContentController::RemoveUserScript(world->identifier(), userScript.identifier()), identifier());
m_userScripts->elements().removeAll(&userScript);
-
- removeContentWorldUses(world.get(), 1);
}
void WebUserContentControllerProxy::removeAllUserScripts(API::ContentWorld& world)
@@ -226,11 +209,9 @@
for (auto& process : m_processes)
process.send(Messages::WebUserContentController::RemoveAllUserScripts({ world.identifier() }), identifier());
- unsigned userScriptsRemoved = m_userScripts->removeAllOfTypeMatching<API::UserScript>([&](const auto& userScript) {
+ m_userScripts->removeAllOfTypeMatching<API::UserScript>([&](const auto& userScript) {
return &userScript->contentWorld() == &world;
});
-
- removeContentWorldUses(world, userScriptsRemoved);
}
void WebUserContentControllerProxy::removeAllUserScripts()
@@ -248,8 +229,6 @@
process.send(Messages::WebUserContentController::RemoveAllUserScripts(worldIdentifiers), identifier());
m_userScripts->elements().clear();
-
- removeContentWorldUses(worlds);
}
void WebUserContentControllerProxy::addUserStyleSheet(API::UserStyleSheet& userStyleSheet)
@@ -256,7 +235,7 @@
{
Ref<API::ContentWorld> world = userStyleSheet.contentWorld();
- addContentWorldUse(world.get());
+ addContentWorld(world.get());
m_userStyleSheets->elements().append(&userStyleSheet);
@@ -272,8 +251,6 @@
process.send(Messages::WebUserContentController::RemoveUserStyleSheet(world->identifier(), userStyleSheet.identifier()), identifier());
m_userStyleSheets->elements().removeAll(&userStyleSheet);
-
- removeContentWorldUses(world.get(), 1);
}
void WebUserContentControllerProxy::removeAllUserStyleSheets(API::ContentWorld& world)
@@ -281,11 +258,9 @@
for (auto& process : m_processes)
process.send(Messages::WebUserContentController::RemoveAllUserStyleSheets({ world.identifier() }), identifier());
- unsigned userStyleSheetsRemoved = m_userStyleSheets->removeAllOfTypeMatching<API::UserStyleSheet>([&](const auto& userStyleSheet) {
+ m_userStyleSheets->removeAllOfTypeMatching<API::UserStyleSheet>([&](const auto& userStyleSheet) {
return &userStyleSheet->contentWorld() == &world;
});
-
- removeContentWorldUses(world, userStyleSheetsRemoved);
}
void WebUserContentControllerProxy::removeAllUserStyleSheets()
@@ -303,8 +278,6 @@
process.send(Messages::WebUserContentController::RemoveAllUserStyleSheets(worldIdentifiers), identifier());
m_userStyleSheets->elements().clear();
-
- removeContentWorldUses(worlds);
}
bool WebUserContentControllerProxy::addUserScriptMessageHandler(WebScriptMessageHandler& handler)
@@ -316,7 +289,7 @@
return false;
}
- addContentWorldUse(world);
+ addContentWorld(world);
m_scriptMessageHandlers.add(handler.identifier(), &handler);
@@ -335,7 +308,6 @@
m_scriptMessageHandlers.remove(it);
- removeContentWorldUses(world, 1);
return;
}
}
@@ -354,8 +326,6 @@
}
return false;
});
-
- removeContentWorldUses(world, numberRemoved);
}
void WebUserContentControllerProxy::didPostMessage(IPC::Connection& connection, WebPageProxyIdentifier pageProxyID, FrameInfoData&& frameInfoData, uint64_t messageHandlerID, const IPC::DataReference& dataReference)
Modified: trunk/Source/WebKit/UIProcess/UserContent/WebUserContentControllerProxy.h (260329 => 260330)
--- trunk/Source/WebKit/UIProcess/UserContent/WebUserContentControllerProxy.h 2020-04-18 23:32:19 UTC (rev 260329)
+++ trunk/Source/WebKit/UIProcess/UserContent/WebUserContentControllerProxy.h 2020-04-19 02:08:52 UTC (rev 260330)
@@ -26,6 +26,7 @@
#pragma once
#include "APIObject.h"
+#include "ContentWorldShared.h"
#include "MessageReceiver.h"
#include "UserContentControllerIdentifier.h"
#include "WebPageProxyIdentifier.h"
@@ -93,8 +94,6 @@
void removeAllUserStyleSheets(API::ContentWorld&);
void removeAllUserStyleSheets();
- void removeAllUserContent(API::ContentWorld&);
-
// Returns false if there was a name conflict.
bool addUserScriptMessageHandler(WebScriptMessageHandler&);
void removeUserMessageHandlerForName(const String&, API::ContentWorld&);
@@ -113,6 +112,8 @@
UserContentControllerIdentifier identifier() const { return m_identifier; }
+ void contentWorldDestroyed(API::ContentWorld&);
+
private:
// IPC::MessageReceiver.
void didReceiveMessage(IPC::Connection&, IPC::Decoder&) override;
@@ -119,10 +120,7 @@
void didPostMessage(IPC::Connection&, WebPageProxyIdentifier, FrameInfoData&&, uint64_t messageHandlerID, const IPC::DataReference&);
- void addContentWorldUse(API::ContentWorld&);
- void removeContentWorldUses(API::ContentWorld&, unsigned numberOfUsesToRemove);
- void removeContentWorldUses(HashCountedSet<RefPtr<API::ContentWorld>>&);
- bool shouldSendRemoveContentWorldsMessage(API::ContentWorld&, unsigned numberOfUsesToRemove);
+ void addContentWorld(API::ContentWorld&);
UserContentControllerIdentifier m_identifier;
WeakHashSet<WebProcessProxy> m_processes;
@@ -129,7 +127,7 @@
Ref<API::Array> m_userScripts;
Ref<API::Array> m_userStyleSheets;
HashMap<uint64_t, RefPtr<WebScriptMessageHandler>> m_scriptMessageHandlers;
- HashCountedSet<RefPtr<API::ContentWorld>> m_contentWorlds;
+ HashSet<ContentWorldIdentifier> m_associatedContentWorlds;
#if ENABLE(CONTENT_EXTENSIONS)
WeakHashSet<NetworkProcessProxy> m_networkProcesses;
Modified: trunk/Tools/ChangeLog (260329 => 260330)
--- trunk/Tools/ChangeLog 2020-04-18 23:32:19 UTC (rev 260329)
+++ trunk/Tools/ChangeLog 2020-04-19 02:08:52 UTC (rev 260330)
@@ -1,3 +1,15 @@
+2020-04-18 Brady Eidson <beid...@apple.com>
+
+ Fix WebUserContentControllerProxy vs ContentWorld lifetime
+ https://bugs.webkit.org/show_bug.cgi?id=210700
+
+ Reviewed by Alex Christensen.
+
+ * TestWebKitAPI/Tests/WebKitCocoa/WKWebViewEvaluateJavaScript.mm:
+ (-[DummyMessageHandler userContentController:didReceiveScriptMessage:]):
+ (TEST): Make sure removing a script message handler from a particular world
+ doesn't also destroy the other _javascript_ contents of that world.
+
2020-04-18 Daniel Bates <daba...@apple.com>
Add some more tests for -focusTextInputContext:placeCaretAt:completionHandler:
Modified: trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/WKWebViewEvaluateJavaScript.mm (260329 => 260330)
--- trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/WKWebViewEvaluateJavaScript.mm 2020-04-18 23:32:19 UTC (rev 260329)
+++ trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/WKWebViewEvaluateJavaScript.mm 2020-04-19 02:08:52 UTC (rev 260330)
@@ -34,11 +34,12 @@
#import "TestURLSchemeHandler.h"
#import "TestWKWebView.h"
#import "WKWebViewConfigurationExtras.h"
-#import <WebKit/WKWebViewConfigurationPrivate.h>
-#import <WebKit/WKContentWorld.h>
+#import <WebKit/WKContentWorldPrivate.h>
#import <WebKit/WKErrorPrivate.h>
#import <WebKit/WKPreferencesPrivate.h>
#import <WebKit/WKPreferencesRef.h>
+#import <WebKit/WKUserContentControllerPrivate.h>
+#import <WebKit/WKWebViewConfigurationPrivate.h>
#import <WebKit/WKWebViewPrivate.h>
#import <WebKit/_WKFrameTreeNode.h>
#import <wtf/RetainPtr.h>
@@ -133,6 +134,15 @@
EXPECT_EQ(namedWorld, [WKContentWorld worldWithName:@"Name"]);
}
+@interface DummyMessageHandler : NSObject <WKScriptMessageHandler>
+@end
+
+@implementation DummyMessageHandler
+- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
+{
+}
+@end
+
TEST(WKWebView, EvaluateJavaScriptInWorlds)
{
RetainPtr<TestWKWebView> webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600)]);
@@ -185,8 +195,12 @@
TestWebKitAPI::Util::run(&isDone);
isDone = false;
- // Set a varibale value in a named world.
+ // Add a scriptMessageHandler in a named world.
RetainPtr<WKContentWorld> namedWorld = [WKContentWorld worldWithName:@"NamedWorld"];
+ id handler = [[[DummyMessageHandler alloc] init] autorelease];
+ [webView.get().configuration.userContentController _addScriptMessageHandler:handler name:@"testHandlerName" userContentWorld:namedWorld.get()._userContentWorld];
+
+ // Set a variable value in that named world.
[webView evaluateJavaScript:@"var bar = 'baz'" inContentWorld:namedWorld.get() completionHandler:^(id result, NSError *error) {
EXPECT_NULL(result);
isDone = true;
@@ -195,7 +209,7 @@
TestWebKitAPI::Util::run(&isDone);
isDone = false;
- // Set a global variable value in a named world via a function call.
+ // Set a global variable value in that named world via a function call.
[webView callAsyncJavaScript:@"window.baz = 'bat'" arguments:nil inContentWorld:namedWorld.get() completionHandler:^(id result, NSError *error) {
EXPECT_NULL(result);
EXPECT_NULL(error);
@@ -205,7 +219,10 @@
TestWebKitAPI::Util::run(&isDone);
isDone = false;
- // Verify they are there in that named world.
+ // Remove the dummy message handler
+ [webView.get().configuration.userContentController _removeScriptMessageHandlerForName:@"testHandlerName" userContentWorld:namedWorld.get()._userContentWorld];
+
+ // Verify the variables we set are there in that named world.
[webView evaluateJavaScript:@"bar" inContentWorld:namedWorld.get() completionHandler:^(id result, NSError *error) {
EXPECT_TRUE([result isKindOfClass:[NSString class]]);
EXPECT_TRUE([result isEqualToString:@"baz"]);