Extend URL filter's Term definition to support groups/subpatterns
authorbenjamin@webkit.org <benjamin@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 10 Mar 2015 20:09:26 +0000 (20:09 +0000)
committerbenjamin@webkit.org <benjamin@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 10 Mar 2015 20:09:26 +0000 (20:09 +0000)
https://bugs.webkit.org/show_bug.cgi?id=142519

Patch by Benjamin Poulain <bpoulain@apple.com> on 2015-03-10
Reviewed by Alex Christensen.

Source/WebCore:

Pretty simple extension: Term is extended to support holding
a Vector of Term. The quantifier of the Term applies to its
Vector of term as a whole.

To avoid exposing too much internal in the API of Term, I moved
graph generation from GraphBuilder to Term.

Sinking a CharacterSet works as usual. Sinking a Group is done
by sinking each of its Terms one by one and then apply the quantifier
on the whole subgraph. This is done by recursively calling into
Term::generateGraph().

Since groups could be nested, the groups make a stack with the latest
open group on top.
When sinking a floating Term, it is sunk to the latest open group. If there is no open
group, we use the prefix tree and sink the whole subpattern to the graph.

* contentextensions/URLFilterParser.cpp:
(WebCore::ContentExtensions::Term::Term):
(WebCore::ContentExtensions::Term::extendGroupSubpattern):
(WebCore::ContentExtensions::Term::generateGraph):
(WebCore::ContentExtensions::Term::operator==):
(WebCore::ContentExtensions::Term::hash):
(WebCore::ContentExtensions::Term::isUniversalTransition):
(WebCore::ContentExtensions::Term::generateSubgraphForAtom):
(WebCore::ContentExtensions::Term::destroy):
(WebCore::ContentExtensions::Term::Group::operator==):
(WebCore::ContentExtensions::Term::Group::hash):
(WebCore::ContentExtensions::GraphBuilder::finalize):
(WebCore::ContentExtensions::GraphBuilder::atomParenthesesSubpatternBegin):
(WebCore::ContentExtensions::GraphBuilder::atomParenthesesEnd):
(WebCore::ContentExtensions::GraphBuilder::sinkFloatingTermIfNecessary):
(WebCore::ContentExtensions::Term::quantifier): Deleted.
(WebCore::ContentExtensions::Term::visitSimpleTransitions): Deleted.
(WebCore::ContentExtensions::GraphBuilder::addTransitions): Deleted.
(WebCore::ContentExtensions::GraphBuilder::sinkFloatingTerm): Deleted.

Tools:

* TestWebKitAPI/Tests/WebCore/ContentExtensions.cpp:
(TestWebKitAPI::testURL):

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@181341 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Source/WebCore/ChangeLog
Source/WebCore/contentextensions/URLFilterParser.cpp
Tools/ChangeLog
Tools/TestWebKitAPI/Tests/WebCore/ContentExtensions.cpp

index c0e3552..f626ad6 100644 (file)
@@ -1,3 +1,47 @@
+2015-03-10  Benjamin Poulain  <bpoulain@apple.com>
+
+        Extend URL filter's Term definition to support groups/subpatterns
+        https://bugs.webkit.org/show_bug.cgi?id=142519
+
+        Reviewed by Alex Christensen.
+
+        Pretty simple extension: Term is extended to support holding
+        a Vector of Term. The quantifier of the Term applies to its
+        Vector of term as a whole.
+
+        To avoid exposing too much internal in the API of Term, I moved
+        graph generation from GraphBuilder to Term.
+
+        Sinking a CharacterSet works as usual. Sinking a Group is done
+        by sinking each of its Terms one by one and then apply the quantifier
+        on the whole subgraph. This is done by recursively calling into
+        Term::generateGraph().
+
+        Since groups could be nested, the groups make a stack with the latest
+        open group on top.
+        When sinking a floating Term, it is sunk to the latest open group. If there is no open
+        group, we use the prefix tree and sink the whole subpattern to the graph.
+
+        * contentextensions/URLFilterParser.cpp:
+        (WebCore::ContentExtensions::Term::Term):
+        (WebCore::ContentExtensions::Term::extendGroupSubpattern):
+        (WebCore::ContentExtensions::Term::generateGraph):
+        (WebCore::ContentExtensions::Term::operator==):
+        (WebCore::ContentExtensions::Term::hash):
+        (WebCore::ContentExtensions::Term::isUniversalTransition):
+        (WebCore::ContentExtensions::Term::generateSubgraphForAtom):
+        (WebCore::ContentExtensions::Term::destroy):
+        (WebCore::ContentExtensions::Term::Group::operator==):
+        (WebCore::ContentExtensions::Term::Group::hash):
+        (WebCore::ContentExtensions::GraphBuilder::finalize):
+        (WebCore::ContentExtensions::GraphBuilder::atomParenthesesSubpatternBegin):
+        (WebCore::ContentExtensions::GraphBuilder::atomParenthesesEnd):
+        (WebCore::ContentExtensions::GraphBuilder::sinkFloatingTermIfNecessary):
+        (WebCore::ContentExtensions::Term::quantifier): Deleted.
+        (WebCore::ContentExtensions::Term::visitSimpleTransitions): Deleted.
+        (WebCore::ContentExtensions::GraphBuilder::addTransitions): Deleted.
+        (WebCore::ContentExtensions::GraphBuilder::sinkFloatingTerm): Deleted.
+
 2015-03-10  Roger Fong  <roger_fong@apple.com>
 
         Adjustments to media control fonts.
index 26c3f46..4c7d9ce 100644 (file)
@@ -31,6 +31,7 @@
 #include "NFA.h"
 #include <JavaScriptCore/YarrParser.h>
 #include <wtf/BitVector.h>
+#include <wtf/Deque.h>
 
 namespace WebCore {
 
@@ -66,13 +67,20 @@ public:
     }
 
     enum CharacterSetTermTag { CharacterSetTerm };
-    Term(CharacterSetTermTag, bool isInverted)
+    explicit Term(CharacterSetTermTag, bool isInverted)
         : m_termType(TermType::CharacterSet)
     {
         new (NotNull, &m_atomData.characterSet) CharacterSet();
         m_atomData.characterSet.inverted = isInverted;
     }
 
+    enum GroupTermTag { GroupTerm };
+    explicit Term(GroupTermTag)
+        : m_termType(TermType::Group)
+    {
+        new (NotNull, &m_atomData.group) Group();
+    }
+
     Term(const Term& other)
         : m_termType(other.m_termType)
         , m_quantifier(other.m_quantifier)
@@ -84,6 +92,9 @@ public:
         case TermType::CharacterSet:
             new (NotNull, &m_atomData.characterSet) CharacterSet(other.m_atomData.characterSet);
             break;
+        case TermType::Group:
+            new (NotNull, &m_atomData.group) Group(other.m_atomData.group);
+            break;
         }
     }
 
@@ -98,6 +109,9 @@ public:
         case TermType::CharacterSet:
             new (NotNull, &m_atomData.characterSet) CharacterSet(WTF::move(other.m_atomData.characterSet));
             break;
+        case TermType::Group:
+            new (NotNull, &m_atomData.group) Group(WTF::move(other.m_atomData.group));
+            break;
         }
         other.destroy();
     }
@@ -140,39 +154,61 @@ public:
         }
     }
 
+    void extendGroupSubpattern(const Term& term)
+    {
+        ASSERT_WITH_SECURITY_IMPLICATION(m_termType == TermType::Group);
+        if (m_termType != TermType::Group)
+            return;
+        m_atomData.group.terms.append(term);
+    }
+
     void quantify(const AtomQuantifier& quantifier)
     {
         ASSERT_WITH_MESSAGE(m_quantifier == AtomQuantifier::One, "Transition to quantified term should only happen once.");
         m_quantifier = quantifier;
     }
 
-    AtomQuantifier quantifier() const
+    unsigned generateGraph(NFA& nfa, uint64_t patternId, unsigned start) const
     {
-        return m_quantifier;
-    }
+        ASSERT(isValid());
 
-    bool isUniversalTransition() const
-    {
-        return m_termType == TermType::CharacterSet
-            && ((m_atomData.characterSet.inverted && !m_atomData.characterSet.characters.bitCount())
-                || (!m_atomData.characterSet.inverted && m_atomData.characterSet.characters.bitCount() == 128));
-    }
+        switch (m_quantifier) {
+        case AtomQuantifier::One: {
+            unsigned newEnd = generateSubgraphForAtom(nfa, patternId, start);
+            return newEnd;
+        }
+        case AtomQuantifier::ZeroOrOne: {
+            unsigned newEnd = generateSubgraphForAtom(nfa, patternId, start);
+            nfa.addEpsilonTransition(start, newEnd);
+            return newEnd;
+        }
+        case AtomQuantifier::ZeroOrMore: {
+            unsigned repeatStart = nfa.createNode();
+            nfa.addRuleId(repeatStart, patternId);
+            nfa.addEpsilonTransition(start, repeatStart);
 
-    void visitSimpleTransitions(std::function<void(char)> visitor) const
-    {
-        ASSERT_WITH_SECURITY_IMPLICATION(m_termType == TermType::CharacterSet);
-        if (m_termType != TermType::CharacterSet)
-            return;
+            unsigned repeatEnd = generateSubgraphForAtom(nfa, patternId, repeatStart);
+            nfa.addEpsilonTransition(repeatEnd, repeatStart);
 
-        if (!m_atomData.characterSet.inverted) {
-            for (const auto& characterIterator : m_atomData.characterSet.characters.setBits())
-                visitor(static_cast<char>(characterIterator));
-        } else {
-            for (unsigned i = 1; i < m_atomData.characterSet.characters.size(); ++i) {
-                if (m_atomData.characterSet.characters.get(i))
-                    continue;
-                visitor(static_cast<char>(i));
-            }
+            unsigned kleenEnd = nfa.createNode();
+            nfa.addRuleId(kleenEnd, patternId);
+            nfa.addEpsilonTransition(repeatEnd, kleenEnd);
+            nfa.addEpsilonTransition(start, kleenEnd);
+            return kleenEnd;
+        }
+        case AtomQuantifier::OneOrMore: {
+            unsigned repeatStart = nfa.createNode();
+            nfa.addRuleId(repeatStart, patternId);
+            nfa.addEpsilonTransition(start, repeatStart);
+
+            unsigned repeatEnd = generateSubgraphForAtom(nfa, patternId, repeatStart);
+            nfa.addEpsilonTransition(repeatEnd, repeatStart);
+
+            unsigned afterRepeat = nfa.createNode();
+            nfa.addRuleId(afterRepeat, patternId);
+            nfa.addEpsilonTransition(repeatEnd, afterRepeat);
+            return afterRepeat;
+        }
         }
     }
 
@@ -201,6 +237,8 @@ public:
             return true;
         case TermType::CharacterSet:
             return m_atomData.characterSet == other.m_atomData.characterSet;
+        case TermType::Group:
+            return m_atomData.group == other.m_atomData.group;
         }
         ASSERT_NOT_REACHED();
         return false;
@@ -220,6 +258,9 @@ public:
         case TermType::CharacterSet:
             secondary = m_atomData.characterSet.hash();
             break;
+        case TermType::Group:
+            secondary = m_atomData.group.hash();
+            break;
         }
         return WTF::pairIntHash(primary, secondary);
     }
@@ -235,6 +276,48 @@ public:
     }
 
 private:
+    bool isUniversalTransition() const
+    {
+        return m_termType == TermType::CharacterSet
+            && ((m_atomData.characterSet.inverted && !m_atomData.characterSet.characters.bitCount())
+                || (!m_atomData.characterSet.inverted && m_atomData.characterSet.characters.bitCount() == 128));
+    }
+
+    unsigned generateSubgraphForAtom(NFA& nfa, uint64_t patternId, unsigned source) const
+    {
+        switch (m_termType) {
+        case TermType::Empty:
+        case TermType::Deleted:
+            ASSERT_NOT_REACHED();
+            return -1;
+        case TermType::CharacterSet: {
+            unsigned target = nfa.createNode();
+            nfa.addRuleId(target, patternId);
+            if (isUniversalTransition())
+                nfa.addTransitionsOnAnyCharacter(source, target);
+            else {
+                if (!m_atomData.characterSet.inverted) {
+                    for (const auto& characterIterator : m_atomData.characterSet.characters.setBits())
+                        nfa.addTransition(source, target, static_cast<char>(characterIterator));
+                } else {
+                    for (unsigned i = 1; i < m_atomData.characterSet.characters.size(); ++i) {
+                        if (m_atomData.characterSet.characters.get(i))
+                            continue;
+                        nfa.addTransition(source, target, static_cast<char>(i));
+                    }
+                }
+            }
+            return target;
+        }
+        case TermType::Group: {
+            unsigned lastTarget = source;
+            for (const Term& term : m_atomData.group.terms)
+                lastTarget = term.generateGraph(nfa, patternId, lastTarget);
+            return lastTarget;
+        }
+        }
+    }
+
     void destroy()
     {
         switch (m_termType) {
@@ -244,6 +327,9 @@ private:
         case TermType::CharacterSet:
             m_atomData.characterSet.~CharacterSet();
             break;
+        case TermType::Group:
+            m_atomData.group.~Group();
+            break;
         }
         m_termType = TermType::Deleted;
     }
@@ -251,7 +337,8 @@ private:
     enum class TermType : uint8_t {
         Empty,
         Deleted,
-        CharacterSet
+        CharacterSet,
+        Group
     };
 
     TermType m_termType { TermType::Empty };
@@ -272,6 +359,26 @@ private:
         }
     };
 
+    struct Group {
+        Vector<Term> terms;
+
+        bool operator==(const Group& other) const
+        {
+            return other.terms == terms;
+        }
+
+        unsigned hash() const
+        {
+            unsigned hash = 6421749;
+            for (const Term& term : terms) {
+                unsigned termHash = term.hash();
+                hash = (hash << 16) ^ ((termHash << 11) ^ hash);
+                hash += hash >> 11;
+            }
+            return hash;
+        }
+    };
+
     union AtomData {
         AtomData()
             : invalidTerm(0)
@@ -283,6 +390,7 @@ private:
 
         char invalidTerm;
         CharacterSet characterSet;
+        Group group;
     } m_atomData;
 };
 
@@ -318,6 +426,11 @@ public:
 
         sinkFloatingTermIfNecessary();
 
+        if (!m_openGroups.isEmpty()) {
+            fail(ASCIILiteral("The expression has unclosed groups."));
+            return;
+        }
+
         if (m_subtreeStart != m_subtreeEnd)
             m_nfa.setFinal(m_subtreeEnd, m_patternId);
         else
@@ -448,7 +561,12 @@ public:
 
     void atomParenthesesSubpatternBegin(bool = true)
     {
-        fail(ASCIILiteral("Groups are not supported yet."));
+        if (hasError())
+            return;
+
+        sinkFloatingTermIfNecessary();
+
+        m_openGroups.append(Term(Term::GroupTerm));
     }
 
     void atomParentheticalAssertionBegin(bool = false)
@@ -458,7 +576,13 @@ public:
 
     void atomParenthesesEnd()
     {
-        fail(ASCIILiteral("Groups are not supported yet."));
+        if (hasError())
+            return;
+
+        sinkFloatingTermIfNecessary();
+        ASSERT(!m_floatingTerm.isValid());
+
+        m_floatingTerm = m_openGroups.takeLast();
     }
 
     void disjunction()
@@ -483,68 +607,6 @@ private:
         m_errorMessage = errorMessage;
     }
 
-    void addTransitions(unsigned source, unsigned target)
-    {
-        auto visitor = [this, source, target](char character) {
-            if (m_floatingTerm.isUniversalTransition())
-                m_nfa.addTransitionsOnAnyCharacter(source, target);
-            else
-                m_nfa.addTransition(source, target, character);
-        };
-        m_floatingTerm.visitSimpleTransitions(visitor);
-    }
-
-    unsigned sinkFloatingTerm(unsigned start)
-    {
-        switch (m_floatingTerm.quantifier()) {
-        case AtomQuantifier::One: {
-            unsigned newEnd = m_nfa.createNode();
-            m_nfa.addRuleId(newEnd, m_patternId);
-            addTransitions(start, newEnd);
-            return newEnd;
-        }
-        case AtomQuantifier::ZeroOrOne: {
-            unsigned newEnd = m_nfa.createNode();
-            m_nfa.addRuleId(newEnd, m_patternId);
-            addTransitions(start, newEnd);
-            return newEnd;
-        }
-        case AtomQuantifier::ZeroOrMore: {
-            unsigned repeatStart = m_nfa.createNode();
-            m_nfa.addRuleId(repeatStart, m_patternId);
-            unsigned repeatEnd = m_nfa.createNode();
-            m_nfa.addRuleId(repeatEnd, m_patternId);
-
-            addTransitions(repeatStart, repeatEnd);
-            m_nfa.addEpsilonTransition(repeatEnd, repeatStart);
-
-            m_nfa.addEpsilonTransition(start, repeatStart);
-
-            unsigned kleenEnd = m_nfa.createNode();
-            m_nfa.addRuleId(kleenEnd, m_patternId);
-            m_nfa.addEpsilonTransition(repeatEnd, kleenEnd);
-            m_nfa.addEpsilonTransition(start, kleenEnd);
-            return kleenEnd;
-        }
-        case AtomQuantifier::OneOrMore: {
-            unsigned repeatStart = m_nfa.createNode();
-            m_nfa.addRuleId(repeatStart, m_patternId);
-            unsigned repeatEnd = m_nfa.createNode();
-            m_nfa.addRuleId(repeatEnd, m_patternId);
-
-            addTransitions(repeatStart, repeatEnd);
-            m_nfa.addEpsilonTransition(repeatEnd, repeatStart);
-
-            m_nfa.addEpsilonTransition(start, repeatStart);
-
-            unsigned afterRepeat = m_nfa.createNode();
-            m_nfa.addRuleId(afterRepeat, m_patternId);
-            m_nfa.addEpsilonTransition(repeatEnd, afterRepeat);
-            return afterRepeat;
-        }
-        }
-    }
-
     void sinkFloatingTermIfNecessary()
     {
         if (!m_floatingTerm.isValid())
@@ -552,6 +614,12 @@ private:
 
         ASSERT(m_lastPrefixTreeEntry);
 
+        if (!m_openGroups.isEmpty()) {
+            m_openGroups.last().extendGroupSubpattern(m_floatingTerm);
+            m_floatingTerm = Term();
+            return;
+        }
+
         auto nextEntry = m_lastPrefixTreeEntry->nextPattern.find(m_floatingTerm);
         if (nextEntry != m_lastPrefixTreeEntry->nextPattern.end()) {
             m_lastPrefixTreeEntry = nextEntry->value.get();
@@ -559,7 +627,7 @@ private:
         } else {
             std::unique_ptr<PrefixTreeEntry> nextPrefixTreeEntry = std::make_unique<PrefixTreeEntry>();
 
-            unsigned newEnd = sinkFloatingTerm(m_lastPrefixTreeEntry->nfaNode);
+            unsigned newEnd = m_floatingTerm.generateGraph(m_nfa, m_patternId, m_lastPrefixTreeEntry->nfaNode);
             nextPrefixTreeEntry->nfaNode = newEnd;
 
             auto addResult = m_lastPrefixTreeEntry->nextPattern.set(m_floatingTerm, WTF::move(nextPrefixTreeEntry));
@@ -586,6 +654,7 @@ private:
     unsigned m_subtreeEnd { 0 };
 
     PrefixTreeEntry* m_lastPrefixTreeEntry;
+    Deque<Term> m_openGroups;
     Term m_floatingTerm;
 
     PrefixTreeEntry* m_newPrefixSubtreeRoot = nullptr;
index eadd81f..7ac28bf 100644 (file)
@@ -1,3 +1,13 @@
+2015-03-10  Benjamin Poulain  <bpoulain@apple.com>
+
+        Extend URL filter's Term definition to support groups/subpatterns
+        https://bugs.webkit.org/show_bug.cgi?id=142519
+
+        Reviewed by Alex Christensen.
+
+        * TestWebKitAPI/Tests/WebCore/ContentExtensions.cpp:
+        (TestWebKitAPI::testURL):
+
 2015-03-06  Jer Noble  <jer.noble@apple.com>
 
         Add an option to run-webkit-tests to override the LayoutTests/ directory
index b0da643..d8a26bd 100644 (file)
@@ -92,6 +92,17 @@ private:
     ContentExtensions::CompiledContentExtensionData m_data;
 };
 
+void static testURL(ContentExtensions::ContentExtensionsBackend contentExtensionsBackend, const URL& url, Vector<ContentExtensions::ActionType> expectedActions)
+{
+    auto actions = contentExtensionsBackend.actionsForURL(url);
+    EXPECT_EQ(expectedActions.size(), actions.size());
+    if (expectedActions.size() != actions.size())
+        return;
+
+    for (unsigned i = 0; i < expectedActions.size(); ++i)
+        EXPECT_EQ(expectedActions[i], actions[i].type());
+}
+
 const char* basicFilter = "[{\"action\":{\"type\":\"block\"},\"trigger\":{\"url-filter\":\".*webkit.org\"}}]";
 
 TEST_F(ContentExtensionTest, Basic)
@@ -101,10 +112,49 @@ TEST_F(ContentExtensionTest, Basic)
 
     ContentExtensions::ContentExtensionsBackend backend;
     backend.addContentExtension("testFilter", extension);
-    
-    auto actions = backend.actionsForURL(URL(ParsedURLString, "http://webkit.org/"));
-    EXPECT_EQ(1u, actions.size());
-    EXPECT_EQ(ContentExtensions::ActionType::BlockLoad, actions[0].type());
+
+    testURL(backend, URL(ParsedURLString, "http://webkit.org/"), { ContentExtensions::ActionType::BlockLoad });
+}
+
+const char* patternsStartingWithGroupFilter = "[{\"action\":{\"type\":\"block\"},\"trigger\":{\"url-filter\":\"(http://whatwg\\\\.org/)?webkit\134\134.org\"}}]";
+
+TEST_F(ContentExtensionTest, PatternStartingWithGroup)
+{
+    auto extensionData = ContentExtensions::compileRuleList(patternsStartingWithGroupFilter);
+    auto extension = InMemoryCompiledContentExtension::create(WTF::move(extensionData));
+
+    ContentExtensions::ContentExtensionsBackend backend;
+    backend.addContentExtension("PatternNestedGroupsFilter", extension);
+
+    testURL(backend, URL(URL(), "http://whatwg.org/webkit.org/"), { ContentExtensions::ActionType::BlockLoad });
+    testURL(backend, URL(URL(), "http://whatwg.org/webkit.org"), { ContentExtensions::ActionType::BlockLoad });
+    testURL(backend, URL(URL(), "http://webkit.org/"), { });
+    testURL(backend, URL(URL(), "http://whatwg.org/"), { });
+    testURL(backend, URL(URL(), "http://whatwg.org"), { });
+}
+
+const char* patternNestedGroupsFilter = "[{\"action\":{\"type\":\"block\"},\"trigger\":{\"url-filter\":\"http://webkit\\\\.org/(foo(bar)*)+\"}}]";
+
+TEST_F(ContentExtensionTest, PatternNestedGroups)
+{
+    auto extensionData = ContentExtensions::compileRuleList(patternNestedGroupsFilter);
+    auto extension = InMemoryCompiledContentExtension::create(WTF::move(extensionData));
+
+    ContentExtensions::ContentExtensionsBackend backend;
+    backend.addContentExtension("PatternNestedGroupsFilter", extension);
+
+    testURL(backend, URL(URL(), "http://webkit.org/foo"), { ContentExtensions::ActionType::BlockLoad });
+    testURL(backend, URL(URL(), "http://webkit.org/foobar"), { ContentExtensions::ActionType::BlockLoad });
+    testURL(backend, URL(URL(), "http://webkit.org/foobarbar"), { ContentExtensions::ActionType::BlockLoad });
+    testURL(backend, URL(URL(), "http://webkit.org/foofoobar"), { ContentExtensions::ActionType::BlockLoad });
+    testURL(backend, URL(URL(), "http://webkit.org/foobarfoobar"), { ContentExtensions::ActionType::BlockLoad });
+    testURL(backend, URL(URL(), "http://webkit.org/foob"), { ContentExtensions::ActionType::BlockLoad });
+    testURL(backend, URL(URL(), "http://webkit.org/foor"), { ContentExtensions::ActionType::BlockLoad });
+
+
+    testURL(backend, URL(URL(), "http://webkit.org/"), { });
+    testURL(backend, URL(URL(), "http://webkit.org/bar"), { });
+    testURL(backend, URL(URL(), "http://webkit.org/fobar"), { });
 }
 
 } // namespace TestWebKitAPI