Extract TestFailures's bug-filing code into two new classes
authoraroben@apple.com <aroben@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 12 Jul 2011 13:53:27 +0000 (13:53 +0000)
committeraroben@apple.com <aroben@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 12 Jul 2011 13:53:27 +0000 (13:53 +0000)
Fixes <http://webkit.org/b/64300> TestFailures page's new-bug-filing code is a mess!

Reviewed by Darin Adler and Adam Barth.

* BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/Buildbot.js:
(Buildbot.prototype.resultsDirectoryURL): Changed to return a URI-encoded URL. Otherwise the
URL isn't valid (and it's harder to mock this function).

* BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/Buildbot_unittests.js:
Added. Just tests the above change (for now).

* BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/Builder_unittests.js:
Added a license header, enclosed everything in a closure to avoid polluting the global
namespace, changed the test name to actually describe the passing condition, and replaced
uses of equals() with equal(). (The latter is the actual name of the function, and matches
deepEqual, etc.)

* BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/NewBugForm.js: Added.
(NewBugForm): This class knows how to construct a <form> used for filing new bugs in
Bugzilla based on some parameters.
(NewBugForm.prototype.domElement): Creates and returns the <form> element. Code came from
ViewController.prototype._domForNewAndExistingBugs.

* BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/NewBugForm_unittests.js:
Added. Tests the above code.

* BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/TestFailureBugForm.js: Added.
(TestFailureBugForm): This class knows how to construct a <form> element used for filing new
bugs specifically about test failures. Code came from
ViewController.prototype._domForNewAndExistingBugs.
(TestFailureBugForm.prototype.domElement): Slightly customizes the <form> element returned
by NewBugForm.
(TestFailureBugForm.prototype._computeOperatingSystem):
(TestFailureBugForm.prototype._computePlatform):
(TestFailureBugForm.prototype._createBugTitle):
(TestFailureBugForm.prototype._failingResultsHTMLURL):
(TestFailureBugForm.prototype._failingRevision):
(TestFailureBugForm.prototype._passingRevision):
(TestFailureBugForm.prototype._regressionRangeString):
Code came from ViewController.prototype._domForNewAndExistingBugs. I broke it out into
separate functions to break up the rat's nest a bit.

* BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/TestFailureBugForm_unittests.js:
Added. Tests the above code.

* BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/ViewController.js:
(ViewController.prototype._domForNewAndExistingBugs): Moved code from here to
TestFailureBugForm/NewBugForm, and changed this code to use a TestFailureBugForm.

* BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/index.html: Pull in
NewBugForm/TestFailureBugForm.js.

* BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/run-unittests.html:
Added new tests and required files.

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

Tools/BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/Buildbot.js
Tools/BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/Buildbot_unittests.js [new file with mode: 0644]
Tools/BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/Builder_unittests.js
Tools/BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/NewBugForm.js [new file with mode: 0644]
Tools/BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/NewBugForm_unittests.js [new file with mode: 0644]
Tools/BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/TestFailureBugForm.js [new file with mode: 0644]
Tools/BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/TestFailureBugForm_unittests.js [new file with mode: 0644]
Tools/BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/ViewController.js
Tools/BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/index.html
Tools/BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/run-unittests.html
Tools/ChangeLog

index e9417260e8ad570ed33afa7bad8f11ead41a951e..33bce6e057983097d27c870e2960b55ecb62d22d 100644 (file)
@@ -69,7 +69,7 @@ Buildbot.prototype = {
     },
 
     resultsDirectoryURL: function(builderName, buildName) {
-        return this.baseURL + 'results/' + builderName + '/' + buildName + '/';
+        return this.baseURL + 'results/' + encodeURIComponent(builderName) + '/' + encodeURIComponent(buildName) + '/';
     },
 
     _buildersForNames: function(names) {
diff --git a/Tools/BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/Buildbot_unittests.js b/Tools/BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/Buildbot_unittests.js
new file mode 100644 (file)
index 0000000..5b48fce
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2011 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+(function() {
+
+module('Buildbot');
+
+test('resultsDirectoryURL is URI-encoded', 1, function() {
+    var buildbot = new Buildbot('http://build.example.com/');
+    equal(buildbot.resultsDirectoryURL('Windows 7 Release (Tests)', 'r10 (5)'), 'http://build.example.com/results/Windows%207%20Release%20(Tests)/r10%20(5)/');
+});
+
+})();
index 8d4ef94ee1da585c7489587bf501136219cffbfb..f614bba25887af86967b100b8e4eb468bbaf1f32 100644 (file)
@@ -1,6 +1,33 @@
+/*
+ * Copyright (C) 2011 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+(function() {
+
 module("Builder");
 
-test("getNumberOfFailingTests from Leaks bot", 4, function() {
+test("getNumberOfFailingTests shouldn't include leaks", 4, function() {
     var mockBuildbot = {};
     mockBuildbot.baseURL = 'http://example.com/';
 
@@ -39,12 +66,14 @@ test("getNumberOfFailingTests from Leaks bot", 4, function() {
     };
 
     builder.getNumberOfFailingTests(1, function(failureCount, tooManyFailures) {
-        equals(failureCount, 4);
-        equals(tooManyFailures, false);
+        equal(failureCount, 4);
+        equal(tooManyFailures, false);
     });
 
     window.getResource = realGetResource;
-    equals(window.getResource, realGetResource);
+    equal(window.getResource, realGetResource);
     window.PersistentCache = realPersistentCache;
-    equals(window.PersistentCache, realPersistentCache);
+    equal(window.PersistentCache, realPersistentCache);
 });
+
+})();
diff --git a/Tools/BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/NewBugForm.js b/Tools/BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/NewBugForm.js
new file mode 100644 (file)
index 0000000..889a9c4
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2011 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+function NewBugForm(bugzilla) {
+    this._bugzilla = bugzilla;
+}
+
+NewBugForm.prototype = {
+    domElement: function() {
+        var formData = {};
+
+        if (this.component)
+            formData.component = this.component;
+        if (this.description)
+            formData.comment = this.description;
+        if (this.keywords)
+            formData.keywords = this.keywords;
+        if (this.operatingSystem)
+            formData.op_sys = this.operatingSystem;
+        if (this.platform)
+            formData.rep_platform = this.platform;
+        if (this.product)
+            formData.product = this.product;
+        if (this.title)
+            formData.short_desc = this.title;
+        if (this.url)
+            formData.bug_file_loc = this.url;
+        if (this.version)
+            formData.version = this.version;
+
+        var form = document.createElement('form');
+        form.method = 'POST';
+        form.action = this._bugzilla.baseURL + 'enter_bug.cgi';
+
+        for (var key in formData) {
+            var input = document.createElement('input');
+            input.type = 'hidden';
+            input.name = key;
+            input.value = formData[key];
+            form.appendChild(input);
+        }
+
+        return form;
+    },
+};
diff --git a/Tools/BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/NewBugForm_unittests.js b/Tools/BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/NewBugForm_unittests.js
new file mode 100644 (file)
index 0000000..d2ab5da
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2011 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+(function() {
+
+module('NewBugForm');
+
+const testFormData = {
+    component: { formName: 'component', value: 'Tools / Tests' },
+    description: { formName: 'comment', value: 'This is a description\n\nwith\nnewlines' },
+    keywords: { formName: 'keywords', value: 'Qt, PlatformOnly' },
+    operatingSystem: { formName: 'op_sys', value: 'Windows 7' },
+    platform: { formName: 'rep_platform', value: 'PC' },
+    product: { formName: 'product', value: 'WebKit' },
+    title: { formName: 'short_desc', value: 'This is a bug title' },
+    url: { formName: 'bug_file_loc', value: 'http://example.com/path?query=foo#anchor' },
+    version: { formName: 'version', value: '528+ (Nightly Build)' },
+};
+
+function createTestForm() {
+    var mockBugzilla = {};
+    mockBugzilla.baseURL = 'http://bugs.example.com/';
+
+    var form = new NewBugForm(mockBugzilla);
+    for (var key in testFormData) {
+        form[key] = testFormData[key].value;
+    }
+    return form;
+}
+
+test('properties are set', 9, function() {
+    var form = createTestForm();
+
+    for (var key in testFormData) {
+        equal(form[key], testFormData[key].value);
+    }
+});
+
+test('domElement() posts to enter_bug.cgi', 3, function() {
+    var formElement = createTestForm().domElement();
+    equal(formElement.tagName, 'FORM');
+    equal(formElement.method, 'POST');
+    equal(formElement.action, 'http://bugs.example.com/enter_bug.cgi');
+});
+
+test('domElement() contains only hidden input elements', 9, function() {
+    var elements = createTestForm().domElement().elements;
+    for (var i = 0; i < elements.length; ++i) {
+        equal(elements[i].type, 'hidden');
+    }
+});
+
+test('domElement() contains all form values', 9, function() {
+    var formElement = createTestForm().domElement();
+
+    for (var key in testFormData) {
+        var data = testFormData[key];
+        equal(formElement[data.formName].value, data.value, key);
+    }
+});
+
+})();
diff --git a/Tools/BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/TestFailureBugForm.js b/Tools/BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/TestFailureBugForm.js
new file mode 100644 (file)
index 0000000..877a5b1
--- /dev/null
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2011 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// FIXME: These should probably move to some WebKitBugzilla class (or similar).
+const BugzillaConstants = {
+    Component: {
+        ToolsTests: 'Tools / Tests',
+    },
+
+    Keyword: {
+        LayoutTestFailure: 'LayoutTestFailure',
+        MakingBotsRed: 'MakingBotsRed',
+        Regression: 'Regression',
+    },
+
+    OperatingSystem: {
+        Leopard: 'Mac OS X 10.5',
+        SnowLeopard: 'Mac OS X 10.6',
+        Windows7: 'Windows 7',
+        WindowsXP: 'Windows XP',
+    },
+
+    Platform: {
+        Macintosh: 'Macintosh',
+        PC: 'PC',
+    },
+
+    Product: {
+        WebKit: 'WebKit',
+    },
+
+    Version: {
+        Nightly: '528+ (Nightly Build)',
+    },
+};
+
+function TestFailureBugForm(bugzilla, trac, tester, failingBuildName, passingBuildName, failingTests) {
+    NewBugForm.call(this, bugzilla);
+
+    this._trac = trac;
+    this._tester = tester;
+    this._failingBuildName = failingBuildName;
+    this._passingBuildName = passingBuildName;
+    this._failingTests = failingTests;
+
+    this.component = BugzillaConstants.Component.ToolsTests;
+    this.description = this._createBugDescription();
+    // FIXME: When a newly-added test has been failing since its introduction, it isn't really a
+    // "regression". We should use different keywords in that case. <http://webkit.org/b/61645>
+    this.keywords = [
+        BugzillaConstants.Keyword.LayoutTestFailure,
+        BugzillaConstants.Keyword.MakingBotsRed,
+        BugzillaConstants.Keyword.Regression
+    ].join(', ');
+    this.operatingSystem = this._computeOperatingSystem();
+    this.platform = this._computePlatform();
+    this.product = BugzillaConstants.Product.WebKit;
+    this.title = this._createBugTitle();
+    this.url = this._failingResultsHTMLURL();
+    this.version = BugzillaConstants.Version.Nightly;
+}
+
+TestFailureBugForm.prototype = {
+    domElement: function() {
+        var form = NewBugForm.prototype.domElement.call(this);
+        form.className = 'new-bug-form';
+        form.target = '_blank';
+        return form;
+    },
+
+    _computeOperatingSystem: function() {
+        if (/Windows 7/.test(this._tester.name))
+            return BugzillaConstants.OperatingSystem.Windows7;
+        if (/Windows XP/.test(this._tester.name))
+            return BugzillaConstants.OperatingSystem.WindowsXP;
+        if (/SnowLeopard/.test(this._tester.name))
+            return BugzillaConstants.OperatingSystem.SnowLeopard;
+        if (/Leopard/.test(this._tester.name))
+            return BugzillaConstants.OperatingSystem.Leopard;
+        return '';
+    },
+
+    _computePlatform: function() {
+        if (/Windows/.test(this._tester.name))
+            return BugzillaConstants.Platform.PC;
+        if (/Leopard/.test(this._tester.name))
+            return BugzillaConstants.Platform.Macintosh;
+        return '';
+    },
+
+    _createBugDescription: function() {
+        var firstSuspectRevision = this._passingRevision() ? this._passingRevision() + 1 : this._failingRevision();
+        var lastSuspectRevision = this._failingRevision();
+
+        var endOfFirstSentence;
+        if (this._passingBuildName) {
+            endOfFirstSentence = 'started failing on ' + this._tester.name;
+            if (firstSuspectRevision === lastSuspectRevision)
+                endOfFirstSentence += ' in r' + firstSuspectRevision + ' <' + this._trac.changesetURL(firstSuspectRevision) + '>';
+            else
+                endOfFirstSentence += ' between r' + firstSuspectRevision + ' and r' + lastSuspectRevision + ' (inclusive)';
+        } else
+            endOfFirstSentence = (this._failingTests.length === 1 ? 'has' : 'have') + ' been failing on ' + this._tester.name + ' since at least r' + firstSuspectRevision + ' <' + this._trac.changesetURL(firstSuspectRevision) + '>';
+        var description;
+        if (this._failingTests.length === 1)
+            description = this._failingTests[0] + ' ' + endOfFirstSentence + '.\n\n';
+        else if (this._failingTests.length === 2)
+            description = this._failingTests.join(' and ') + ' ' + endOfFirstSentence + '.\n\n';
+        else {
+            description = 'The following tests ' + endOfFirstSentence + ':\n\n'
+                + this._failingTests.map(function(test) { return '    ' + test }).join('\n')
+                + '\n\n';
+        }
+        if (firstSuspectRevision !== lastSuspectRevision)
+            description += this._trac.logURL('trunk', firstSuspectRevision, lastSuspectRevision) + '\n\n';
+        if (this._passingBuildName)
+            description += this._tester.resultsPageURL(this._passingBuildName) + ' passed\n';
+        description += this._failingResultsHTMLURL() + ' failed\n';
+
+        return description;
+    },
+
+    _createBugTitle: function() {
+        // FIXME: When a newly-added test has been failing since its introduction, it isn't really a
+        // "regression". We should use a different title in that case. <http://webkit.org/b/61645>
+
+        var titlePrefix = 'REGRESSION (' + this._regressionRangeString() + '): ';
+        var titleSuffix = ' failing on ' + this._tester.name;
+        var title = titlePrefix + this._failingTests.join(', ') + titleSuffix;
+
+        if (title.length <= Bugzilla.maximumBugTitleLength)
+            return title;
+
+        var pathPrefix = longestCommonPathPrefix(this._failingTests);
+        if (pathPrefix) {
+            title = titlePrefix + this._failingTests.length + ' ' + pathPrefix + ' tests' + titleSuffix;
+            if (title.length <= Bugzilla.maximumBugTitleLength)
+                return title;
+        }
+
+        title = titlePrefix + this._failingTests.length + ' tests' + titleSuffix;
+
+        console.assert(title.length <= Bugzilla.maximumBugTitleLength);
+        return title;
+    },
+
+    _failingResultsHTMLURL: function() {
+        return this._tester.resultsPageURL(this._failingBuildName);
+    },
+
+    _failingRevision: function() {
+        return this._tester.buildbot.parseBuildName(this._failingBuildName).revision;
+    },
+
+    _passingRevision: function() {
+        if (!this._passingBuildName)
+            return null;
+        return this._tester.buildbot.parseBuildName(this._passingBuildName).revision;
+    },
+
+    _regressionRangeString: function() {
+        var failingRevision = this._failingRevision();
+        var passingRevision = this._passingRevision();
+        if (!passingRevision || failingRevision - passingRevision <= 1)
+            return 'r' + failingRevision;
+        return 'r' + passingRevision + '-r' + failingRevision;
+    },
+};
+
+TestFailureBugForm.prototype.__proto__ = NewBugForm.prototype;
diff --git a/Tools/BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/TestFailureBugForm_unittests.js b/Tools/BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/TestFailureBugForm_unittests.js
new file mode 100644 (file)
index 0000000..fc4b8d6
--- /dev/null
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2011 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+(function() {
+
+module('TestFailureBugForm');
+
+function MockBuilder(name) {
+    this.name = name;
+}
+
+function createTestForm(testerName, failingBuildName, passingBuildName, failingTests) {
+    var mockBugzilla = {};
+    mockBugzilla.baseURL = '[BUGZILLA BASE URL]';
+
+    var mockTrac = {};
+    mockTrac.changesetURL = function(revisionNumber) {
+        return '[CHANGESET URL r' + revisionNumber + ']';
+    }
+    mockTrac.logURL = function(path, startRevision, endRevision) {
+        return '[LOG URL ' + path + ', r' + startRevision + ', r' + endRevision + ']';
+    }
+
+    var mockBuildbot = {};
+    mockBuildbot.parseBuildName = function(buildName) {
+        var match = /(\d+)/.exec(buildName);
+        return {
+            revision: parseInt(match[1], 10),
+            buildNumber: parseInt(match[2], 10),
+        };
+    };
+
+    var mockBuilder = {};
+    mockBuilder.name = testerName;
+    mockBuilder.buildbot = mockBuildbot;
+    mockBuilder.resultsPageURL = function(buildName) {
+        return '[RESULTS PAGE URL ' + this.name + ', ' + buildName + ']';
+    }
+
+    return new TestFailureBugForm(mockBugzilla, mockTrac, mockBuilder, failingBuildName, passingBuildName, failingTests);
+}
+
+test('component and keywords are set', 2, function() {
+    var form = createTestForm('Windows 7 Release (Tests)', 'r10 (5)', 'r8 (2)', ['css1/basic/class_as_selector.html']);
+
+    equal(form.component, BugzillaConstants.Component.ToolsTests);
+    deepEqual(form.keywords.split(', '), [BugzillaConstants.Keyword.LayoutTestFailure, BugzillaConstants.Keyword.MakingBotsRed, BugzillaConstants.Keyword.Regression]);
+});
+
+const testers = {
+    'GTK Linux 32-bit Release': {
+        operatingSystem: '',
+        platform: '',
+    },
+    'Leopard Intel Release (Tests)': {
+        operatingSystem: BugzillaConstants.OperatingSystem.Leopard,
+        platform: BugzillaConstants.Platform.Macintosh,
+    },
+    'SnowLeopard Intel Release (Tests)': {
+        operatingSystem: BugzillaConstants.OperatingSystem.SnowLeopard,
+        platform: BugzillaConstants.Platform.Macintosh,
+    },
+    'Windows 7 Release (Tests)': {
+        operatingSystem: BugzillaConstants.OperatingSystem.Windows7,
+        platform: BugzillaConstants.Platform.PC,
+    },
+    'Windows XP Debug (Tests)': {
+        operatingSystem: BugzillaConstants.OperatingSystem.WindowsXP,
+        platform: BugzillaConstants.Platform.PC,
+    },
+};
+
+test('operating system is deduced', 5, function() {
+    for (var name in testers) {
+        var form = createTestForm(name, 'r10 (5)', 'r8 (2)', ['css1/basic/class_as_selector.html']);
+        equal(form.operatingSystem, testers[name].operatingSystem);
+    }
+});
+
+test('platform is deduced', 5, function() {
+    for (var name in testers) {
+        var form = createTestForm(name, 'r10 (5)', 'r8 (2)', ['css1/basic/class_as_selector.html']);
+        equal(form.platform, testers[name].platform);
+    }
+});
+
+const testCases = [
+    {
+        failingBuildName: 'r10 (5)',
+        failingTests: [
+            'css1/basic/class_as_selector.html',
+        ],
+        expectedDescription: 'css1/basic/class_as_selector.html has been failing on Windows 7 Release (Tests) since at least r10 <[CHANGESET URL r10]>.\n\n[RESULTS PAGE URL Windows 7 Release (Tests), r10 (5)] failed\n',
+        expectedTitle: 'REGRESSION (r10): css1/basic/class_as_selector.html failing on Windows 7 Release (Tests)',
+    },
+    {
+        failingBuildName: 'r10 (5)',
+        passingBuildName: 'r9 (3)',
+        failingTests: [
+            'css1/basic/class_as_selector.html',
+        ],
+        expectedDescription: 'css1/basic/class_as_selector.html started failing on Windows 7 Release (Tests) in r10 <[CHANGESET URL r10]>.\n\n[RESULTS PAGE URL Windows 7 Release (Tests), r9 (3)] passed\n[RESULTS PAGE URL Windows 7 Release (Tests), r10 (5)] failed\n',
+        expectedTitle: 'REGRESSION (r10): css1/basic/class_as_selector.html failing on Windows 7 Release (Tests)',
+    },
+    {
+        failingBuildName: 'r10 (5)',
+        passingBuildName: 'r8 (2)',
+        failingTests: [
+            'css1/basic/class_as_selector.html',
+        ],
+        expectedDescription: 'css1/basic/class_as_selector.html started failing on Windows 7 Release (Tests) between r9 and r10 (inclusive).\n\n[LOG URL trunk, r9, r10]\n\n[RESULTS PAGE URL Windows 7 Release (Tests), r8 (2)] passed\n[RESULTS PAGE URL Windows 7 Release (Tests), r10 (5)] failed\n',
+        expectedTitle: 'REGRESSION (r8-r10): css1/basic/class_as_selector.html failing on Windows 7 Release (Tests)',
+    },
+    {
+        failingBuildName: 'r10 (5)',
+        passingBuildName: 'r8 (2)',
+        failingTests: [
+            'css1/basic/class_as_selector.html',
+            'fast/css/ex-after-font-variant.html',
+        ],
+        expectedDescription: 'css1/basic/class_as_selector.html and fast/css/ex-after-font-variant.html started failing on Windows 7 Release (Tests) between r9 and r10 (inclusive).\n\n[LOG URL trunk, r9, r10]\n\n[RESULTS PAGE URL Windows 7 Release (Tests), r8 (2)] passed\n[RESULTS PAGE URL Windows 7 Release (Tests), r10 (5)] failed\n',
+        expectedTitle: 'REGRESSION (r8-r10): css1/basic/class_as_selector.html, fast/css/ex-after-font-variant.html failing on Windows 7 Release (Tests)',
+    },
+    {
+        failingBuildName: 'r10 (5)',
+        passingBuildName: 'r8 (2)',
+        failingTests: [
+            'css1/basic/class_as_selector1.html',
+            'css1/basic/class_as_selector2.html',
+            'css1/basic/class_as_selector3.html',
+            'css1/basic/class_as_selector4.html',
+            'css1/basic/class_as_selector5.html',
+            'css1/basic/class_as_selector6.html',
+            'css1/basic/class_as_selector7.html',
+            'css1/basic/class_as_selector8.html',
+        ],
+        expectedDescription: 'The following tests started failing on Windows 7 Release (Tests) between r9 and r10 (inclusive):\n\n    css1/basic/class_as_selector1.html\n    css1/basic/class_as_selector2.html\n    css1/basic/class_as_selector3.html\n    css1/basic/class_as_selector4.html\n    css1/basic/class_as_selector5.html\n    css1/basic/class_as_selector6.html\n    css1/basic/class_as_selector7.html\n    css1/basic/class_as_selector8.html\n\n[LOG URL trunk, r9, r10]\n\n[RESULTS PAGE URL Windows 7 Release (Tests), r8 (2)] passed\n[RESULTS PAGE URL Windows 7 Release (Tests), r10 (5)] failed\n',
+        expectedTitle: 'REGRESSION (r8-r10): 8 css1/basic tests failing on Windows 7 Release (Tests)',
+    },
+    {
+        failingBuildName: 'r10 (5)',
+        passingBuildName: 'r8 (2)',
+        failingTests: [
+            'css1/basic/class_as_selector1.html',
+            'css1/basic/class_as_selector2.html',
+            'css1/basic/class_as_selector3.html',
+            'css1/basic/class_as_selector4.html',
+            'css1/basic/class_as_selector5.html',
+            'css1/basic/class_as_selector6.html',
+            'css1/basic/class_as_selector7.html',
+            'css1/basic/class_as_selector8.html',
+            'css1/class_as_selector9.html',
+        ],
+        expectedTitle: 'REGRESSION (r8-r10): 9 css1 tests failing on Windows 7 Release (Tests)',
+    },
+    {
+        failingBuildName: 'r10 (5)',
+        passingBuildName: 'r8 (2)',
+        failingTests: [
+            'css1/basic/class_as_selector1.html',
+            'css1/basic/class_as_selector2.html',
+            'css1/basic/class_as_selector3.html',
+            'css1/basic/class_as_selector4.html',
+            'css1/basic/class_as_selector5.html',
+            'css1/basic/class_as_selector6.html',
+            'css1/basic/class_as_selector7.html',
+            'css1/basic/class_as_selector8.html',
+            'css1/class_as_selector9.html',
+            'fast/css/ex-after-font-variant.html',
+        ],
+        expectedTitle: 'REGRESSION (r8-r10): 10 tests failing on Windows 7 Release (Tests)',
+    },
+];
+
+test('titles', 7, function() {
+    for (var i = 0; i < testCases.length; ++i) {
+        var form = createTestForm('Windows 7 Release (Tests)', testCases[i].failingBuildName, testCases[i].passingBuildName, testCases[i].failingTests);
+        equal(form.title, testCases[i].expectedTitle);
+    }
+});
+
+test('descriptions', 5, function() {
+    for (var i = 0; i < testCases.length; ++i) {
+        if (!('expectedDescription' in testCases[i]))
+            continue;
+        var form = createTestForm('Windows 7 Release (Tests)', testCases[i].failingBuildName, testCases[i].passingBuildName, testCases[i].failingTests);
+        equal(form.description, testCases[i].expectedDescription);
+    }
+});
+
+})();
index 47fcbbee7ffb306f28e6a21407c0a5c1260ec875..069ca32bf62c4365446e8ebbfb82b2b5d5f601fa 100644 (file)
@@ -383,100 +383,10 @@ ViewController.prototype = {
             closedList.appendChildren(closedBugs.map(bugToListItem));
         });
 
-        var parsedFailingBuildName = this._buildbot.parseBuildName(failingBuildName);
-        var regressionRangeString = 'r' + parsedFailingBuildName.revision;
-        if (passingBuildName) {
-            var parsedPassingBuildName = this._buildbot.parseBuildName(passingBuildName);
-            if (parsedFailingBuildName.revision - parsedPassingBuildName.revision > 1)
-                regressionRangeString = 'r' + parsedPassingBuildName.revision + '-' + regressionRangeString;
-        }
+        var bugForm = new TestFailureBugForm(this._bugzilla, this._trac, tester, failingBuildName, passingBuildName, failingTests);
 
-        // FIXME: Some of this code should move into a new method on the Bugzilla class.
-
-        // FIXME: When a newly-added test has been failing since its introduction, it isn't really a
-        // "regression". We should use a different title and keywords in that case.
-        // <http://webkit.org/b/61645>
-
-        var titlePrefix = 'REGRESSION (' + regressionRangeString + '): ';
-        var titleSuffix = ' failing on ' + tester.name;
-        var title = titlePrefix + failingTests.join(', ') + titleSuffix;
-        if (title.length > Bugzilla.maximumBugTitleLength) {
-            var pathPrefix = longestCommonPathPrefix(failingTests);
-            if (pathPrefix)
-                title = titlePrefix + failingTests.length + ' ' + pathPrefix + ' tests' + titleSuffix;
-            if (title.length > Bugzilla.maximumBugTitleLength)
-                title = titlePrefix + failingTests.length + ' tests' + titleSuffix;
-        }
-        console.assert(title.length <= Bugzilla.maximumBugTitleLength);
-
-        var firstSuspectRevision = parsedPassingBuildName ? parsedPassingBuildName.revision + 1 : parsedFailingBuildName.revision;
-        var lastSuspectRevision = parsedFailingBuildName.revision;
-
-        var endOfFirstSentence;
-        if (passingBuildName) {
-            endOfFirstSentence = 'started failing on ' + tester.name;
-            if (firstSuspectRevision === lastSuspectRevision)
-                endOfFirstSentence += ' in r' + firstSuspectRevision + ' <' + this._trac.changesetURL(firstSuspectRevision) + '>';
-            else
-                endOfFirstSentence += ' between r' + firstSuspectRevision + ' and r' + lastSuspectRevision + ' (inclusive)';
-        } else
-            endOfFirstSentence = (failingTests.length === 1 ? 'has' : 'have') + ' been failing on ' + tester.name + ' since at least r' + firstSuspectRevision + ' <' + this._trac.changesetURL(firstSuspectRevision) + '>';
-
-        var description;
-        if (failingTests.length === 1)
-            description = failingTests[0] + ' ' + endOfFirstSentence + '.\n\n';
-        else if (failingTests.length === 2)
-            description = failingTests.join(' and ') + ' ' + endOfFirstSentence + '.\n\n';
-        else {
-            description = 'The following tests ' + endOfFirstSentence + ':\n\n'
-                + failingTests.map(function(test) { return '    ' + test }).join('\n')
-                + '\n\n';
-        }
-        if (firstSuspectRevision !== lastSuspectRevision)
-            description += this._trac.logURL('trunk', firstSuspectRevision, lastSuspectRevision) + '\n\n';
-        if (passingBuildName)
-            description += encodeURI(tester.resultsPageURL(passingBuildName)) + ' passed\n';
-        var failingResultsHTML = tester.resultsPageURL(failingBuildName);
-        description += encodeURI(failingResultsHTML) + ' failed\n';
-
-        var formData = {
-            product: 'WebKit',
-            version: '528+ (Nightly build)',
-            component: 'Tools / Tests',
-            keywords: 'LayoutTestFailure, MakingBotsRed, Regression',
-            short_desc: title,
-            comment: description,
-            bug_file_loc: failingResultsHTML,
-        };
-
-        if (/Windows/.test(tester.name)) {
-            formData.rep_platform = 'PC';
-            if (/Windows 7/.test(tester.name))
-                formData.op_sys = 'Windows 7';
-            else if (/Windows XP/.test(tester.name))
-                formData.op_sys = 'Windows XP';
-        } else if (/Leopard/.test(tester.name)) {
-            formData.rep_platform = 'Macintosh';
-            if (/SnowLeopard/.test(tester.name))
-                formData.op_sys = 'Mac OS X 10.6';
-            else
-                formData.op_sys = 'Mac OS X 10.5';
-        }
-
-        var form = document.createElement('form');
+        var form = bugForm.domElement();
         result.appendChild(form);
-        form.className = 'new-bug-form';
-        form.method = 'POST';
-        form.action = this._bugzilla.baseURL + 'enter_bug.cgi';
-        form.target = '_blank';
-
-        for (var key in formData) {
-            var input = document.createElement('input');
-            input.type = 'hidden';
-            input.name = key;
-            input.value = formData[key];
-            form.appendChild(input);
-        }
 
         var link = document.createElement('a');
         container.appendChild(link);
index 12478122d104ede8c93dac0228bd94cdc2b691da..23258f795531d2064c712f150ecb2762fc4d3ea8 100644 (file)
@@ -34,8 +34,10 @@ THE POSSIBILITY OF SUCH DAMAGE.
     <script src="LayoutTestHistoryAnalyzer.js"></script>
     <script src="LayoutTestResultsLoader.js"></script>
     <script src="NRWTResultsParser.js"></script>
+    <script src="NewBugForm.js"></script>
     <script src="ORWTResultsParser.js"></script>
     <script src="PersistentCache.js"></script>
+    <script src="TestFailureBugForm.js"></script>
     <script src="Trac.js"></script>
     <script src="Utilities.js"></script>
     <script src="ViewController.js"></script>
index 28c934f8c008bc421bd147f2113b55ddd152fdeb..1d4125f638dfb260ea4f33c351ff32e860739abd 100644 (file)
 <div id="qunit-testrunner-toolbar"></div>
 <h2 id="qunit-userAgent"></h2>
 <ol id="qunit-tests"></ol>
+<!-- FIXME: We should have tests for these files! -->
+<script src="Bugzilla.js"></script>
 <script src="Utilities.js"></script>
 
+<script src="Buildbot.js"></script>
+<script src="Buildbot_unittests.js"></script>
 <script src="Builder.js"></script>
 <script src="Builder_unittests.js"></script>
+<script src="NewBugForm.js"></script>
+<script src="NewBugForm_unittests.js"></script>
+<script src="TestFailureBugForm.js"></script>
+<script src="TestFailureBugForm_unittests.js"></script>
 </body>
 </html>
index 2c9e517e6a2173bdaf044a7e48ce812ed758c9d7..0f17628fc0d95c60153d02c90bf3af67c4791655 100644 (file)
@@ -1,3 +1,62 @@
+2011-07-11  Adam Roben  <aroben@apple.com>
+
+        Extract TestFailures's bug-filing code into two new classes
+
+        Fixes <http://webkit.org/b/64300> TestFailures page's new-bug-filing code is a mess!
+
+        Reviewed by Darin Adler and Adam Barth.
+
+        * BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/Buildbot.js:
+        (Buildbot.prototype.resultsDirectoryURL): Changed to return a URI-encoded URL. Otherwise the
+        URL isn't valid (and it's harder to mock this function).
+
+        * BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/Buildbot_unittests.js:
+        Added. Just tests the above change (for now).
+
+        * BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/Builder_unittests.js:
+        Added a license header, enclosed everything in a closure to avoid polluting the global
+        namespace, changed the test name to actually describe the passing condition, and replaced
+        uses of equals() with equal(). (The latter is the actual name of the function, and matches
+        deepEqual, etc.)
+
+        * BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/NewBugForm.js: Added.
+        (NewBugForm): This class knows how to construct a <form> used for filing new bugs in
+        Bugzilla based on some parameters.
+        (NewBugForm.prototype.domElement): Creates and returns the <form> element. Code came from
+        ViewController.prototype._domForNewAndExistingBugs.
+
+        * BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/NewBugForm_unittests.js:
+        Added. Tests the above code.
+
+        * BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/TestFailureBugForm.js: Added.
+        (TestFailureBugForm): This class knows how to construct a <form> element used for filing new
+        bugs specifically about test failures. Code came from
+        ViewController.prototype._domForNewAndExistingBugs.
+        (TestFailureBugForm.prototype.domElement): Slightly customizes the <form> element returned
+        by NewBugForm.
+        (TestFailureBugForm.prototype._computeOperatingSystem):
+        (TestFailureBugForm.prototype._computePlatform):
+        (TestFailureBugForm.prototype._createBugTitle):
+        (TestFailureBugForm.prototype._failingResultsHTMLURL):
+        (TestFailureBugForm.prototype._failingRevision):
+        (TestFailureBugForm.prototype._passingRevision):
+        (TestFailureBugForm.prototype._regressionRangeString):
+        Code came from ViewController.prototype._domForNewAndExistingBugs. I broke it out into
+        separate functions to break up the rat's nest a bit.
+
+        * BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/TestFailureBugForm_unittests.js:
+        Added. Tests the above code.
+
+        * BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/ViewController.js:
+        (ViewController.prototype._domForNewAndExistingBugs): Moved code from here to
+        TestFailureBugForm/NewBugForm, and changed this code to use a TestFailureBugForm.
+
+        * BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/index.html: Pull in
+        NewBugForm/TestFailureBugForm.js.
+
+        * BuildSlaveSupport/build.webkit.org-config/public_html/TestFailures/run-unittests.html:
+        Added new tests and required files.
+
 2011-07-12  Adam Roben  <aroben@apple.com>
 
         Test that no intermediate WTF::Strings are created when concatenating with string literals