Extract the logic to merge tests from MergeTestsHandler and add unit tests
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 28 Feb 2012 02:14:37 +0000 (02:14 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 28 Feb 2012 02:14:37 +0000 (02:14 +0000)
https://bugs.webkit.org/show_bug.cgi?id=79602

Reviewed by Hajime Morita.

Extracted Test.merge and TestResult.replace_to_change_test_name out of MergeTestsHandler,
and moved MergeTestsHandler into admin_handlers.py where it belongs.

Added new backend "model-manipulator" to execute tasks to merge tests.

Also revive the inadvertently removed manual submission form on the admin page.

* Websites/webkit-perf.appspot.com/admin_handlers.py:
(AdminDashboardHandler.get_tests):
(MergeTestsHandler):
(MergeTestsHandler.post):
* Websites/webkit-perf.appspot.com/app.yaml:
* Websites/webkit-perf.appspot.com/backends.yaml: Added.
* Websites/webkit-perf.appspot.com/css/admin.css:
* Websites/webkit-perf.appspot.com/js/admin.js:
* Websites/webkit-perf.appspot.com/main.py:
* Websites/webkit-perf.appspot.com/merge_tests_handler.py: Removed.
* Websites/webkit-perf.appspot.com/models.py:
(Test):
(Test.merge):
(TestResult.replace_to_change_test_name):
* Websites/webkit-perf.appspot.com/models_unittest.py:
(DataStoreTestsBase.assertOnlyInstance):
(DataStoreTestsBase):
(DataStoreTestsBase.assertOnlyInstances):
(DataStoreTestsBase.assertEqualUnorderedModelList):
(DataStoreTestsBase.assertEqualUnorderedList):
(_create_build):
(TestModelTests.test_merge):
(TestResultTests):
(TestResultTests.test_get_or_insert_value):
(TestResultTests.test_get_or_insert_stat_value):
(TestResultTests.test_replace_to_change_test_name):
(TestResultTests.test_replace_to_change_test_name_with_stat_value):
(TestResultTests.test_replace_to_change_test_name_overrides_conflicting_result):

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

ChangeLog
Websites/webkit-perf.appspot.com/admin_handlers.py
Websites/webkit-perf.appspot.com/app.yaml
Websites/webkit-perf.appspot.com/backends.yaml [new file with mode: 0644]
Websites/webkit-perf.appspot.com/css/admin.css
Websites/webkit-perf.appspot.com/js/admin.js
Websites/webkit-perf.appspot.com/main.py
Websites/webkit-perf.appspot.com/merge_tests_handler.py [deleted file]
Websites/webkit-perf.appspot.com/models.py
Websites/webkit-perf.appspot.com/models_unittest.py

index 623036e..2aeb676 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,46 @@
+2012-02-27  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Extract the logic to merge tests from MergeTestsHandler and add unit tests
+        https://bugs.webkit.org/show_bug.cgi?id=79602
+
+        Reviewed by Hajime Morita.
+
+        Extracted Test.merge and TestResult.replace_to_change_test_name out of MergeTestsHandler,
+        and moved MergeTestsHandler into admin_handlers.py where it belongs.
+
+        Added new backend "model-manipulator" to execute tasks to merge tests.
+
+        Also revive the inadvertently removed manual submission form on the admin page.
+
+        * Websites/webkit-perf.appspot.com/admin_handlers.py:
+        (AdminDashboardHandler.get_tests):
+        (MergeTestsHandler):
+        (MergeTestsHandler.post):
+        * Websites/webkit-perf.appspot.com/app.yaml:
+        * Websites/webkit-perf.appspot.com/backends.yaml: Added.
+        * Websites/webkit-perf.appspot.com/css/admin.css:
+        * Websites/webkit-perf.appspot.com/js/admin.js:
+        * Websites/webkit-perf.appspot.com/main.py:
+        * Websites/webkit-perf.appspot.com/merge_tests_handler.py: Removed.
+        * Websites/webkit-perf.appspot.com/models.py:
+        (Test):
+        (Test.merge):
+        (TestResult.replace_to_change_test_name):
+        * Websites/webkit-perf.appspot.com/models_unittest.py:
+        (DataStoreTestsBase.assertOnlyInstance):
+        (DataStoreTestsBase):
+        (DataStoreTestsBase.assertOnlyInstances):
+        (DataStoreTestsBase.assertEqualUnorderedModelList):
+        (DataStoreTestsBase.assertEqualUnorderedList):
+        (_create_build):
+        (TestModelTests.test_merge):
+        (TestResultTests):
+        (TestResultTests.test_get_or_insert_value):
+        (TestResultTests.test_get_or_insert_stat_value):
+        (TestResultTests.test_replace_to_change_test_name):
+        (TestResultTests.test_replace_to_change_test_name_with_stat_value):
+        (TestResultTests.test_replace_to_change_test_name_overrides_conflicting_result):
+
 2012-02-27  ChangSeok Oh  <shivamidow@gmail.com>
 
         [EFL] Support mutation observers
index a34e2a8..8e7ea32 100644 (file)
 import webapp2
 import json
 
+from google.appengine.api import taskqueue
 from google.appengine.api import users
 from google.appengine.ext.db import GqlQuery
 from google.appengine.ext.webapp import template
 
+from controller import schedule_runs_update
+from controller import schedule_dashboard_update
+from controller import schedule_manifest_update
 from models import Branch
 from models import Builder
 from models import Platform
@@ -81,3 +85,42 @@ class AdminDashboardHandler(webapp2.RequestHandler):
     def get_tests(self):
         self.response.headers['Content-Type'] = 'application/json'
         self.response.out.write(json.dumps([test.name for test in Test.all().fetch(limit=200)]))
+
+
+class MergeTestsHandler(webapp2.RequestHandler):
+    def post(self, task):
+        self.response.headers['Content-Type'] = 'text/plain; charset=utf-8'
+
+        if task != 'run':
+            try:
+                payload = json.loads(self.request.body)
+                merge = payload.get('merge', '')
+                into = payload.get('into', '')
+            except:
+                self.response.out.write("Failed to parse the payload: %s" % self.request.body)
+                return
+
+            if merge == into or not Test.get_by_key_name(merge) or not Test.get_by_key_name(into):
+                self.response.out.write('Invalid test names')
+                return
+
+            taskqueue.add(url='/admin/merge-tests/run', params={'merge': merge, 'into': into}, target='model-manipulator')
+            self.response.out.write('OK')
+            return
+
+        merge = Test.get_by_key_name(self.request.get('merge'))
+        into = Test.get_by_key_name(self.request.get('into'))
+
+        branches_and_platforms_to_update = into.merge(merge)
+        if branches_and_platforms_to_update == None:
+            # FIXME: This message is invisible. Need to store this somewhere and let the admin page pull it.
+            self.response.out.write('Cannot merge %s into %s. There are conflicting results.' % (merge.name, into.name))
+            return
+
+        for branch_id, platform_id in branches_and_platforms_to_update:
+            schedule_runs_update(into.id, branch_id, platform_id)
+
+        schedule_dashboard_update()
+        schedule_manifest_update()
+
+        self.response.out.write('OK')
index 4a4a9bf..def9999 100644 (file)
@@ -1,5 +1,5 @@
 application: webkit-perf
-version: 14
+version: 15
 runtime: python27
 api_version: 1
 threadsafe: false
@@ -9,12 +9,6 @@ handlers:
   static_files: favicon.ico
   upload: favicon\.ico
 
-- url: /admin/(.+\.html)
-  static_files: static/\1
-  upload: static
-  secure: always
-  login: admin
-
 - url: /
   static_files: index.html
   upload: index.html
diff --git a/Websites/webkit-perf.appspot.com/backends.yaml b/Websites/webkit-perf.appspot.com/backends.yaml
new file mode 100644 (file)
index 0000000..8aad895
--- /dev/null
@@ -0,0 +1,3 @@
+backends:
+- model-manipulator
+  options: dynamic
index 8c56377..f2e601a 100644 (file)
@@ -4,10 +4,13 @@ html {
     overflow: auto;
 }
 
-#summary {
-    margin-top: 49px;
+#summary, #manual-submission {
     font-size: 14px;
     line-height: 36px;
+}
+
+#summary {
+    margin-top: 49px;
     display: table;
     border-collapse: collapse;
 }
@@ -18,7 +21,6 @@ section {
     border: 1px solid lightgrey;
     border-collapse: collapse;
     min-height: 300px;
-    max-width: 400px;
 }
 
 h2 {
@@ -82,6 +84,15 @@ form dd {
     width: 150px;
 }
 
+#manual-submission form {
+    padding: 0;
+}
+#manual-submission textarea {
+    font-family: monospace;
+    font-size: 14px;
+    border: none;
+}
+
 #footer {
     margin: 10px;
 }
index 63a51a8..2b54595 100644 (file)
@@ -59,9 +59,15 @@ $('form').trigger('reload');
 $('form').bind('submit', function (event) {
     event.preventDefault();
 
-    var contents = {}
-    for (var i = 0; i < this.elements.length; i++)
-        contents[this.elements[i].name] = this.elements[i].value;
+    var payload;
+    if (this.payload)
+        payload = this.payload.value;
+    else {
+        var contents = {};
+        for (var i = 0; i < this.elements.length; i++)
+            contents[this.elements[i].name] = this.elements[i].value;
+        payload = JSON.stringify(contents);
+    }
 
     var xhr = new XMLHttpRequest;
     xhr.onreadystatechange = function () {
@@ -71,10 +77,24 @@ $('form').bind('submit', function (event) {
             error('HTTP status: ' + xhr.status);
         else if (xhr.responseText != 'OK')
             error(xhr.responseText);
-        
     }
     xhr.open(this.method, this.action, true);
-    xhr.send(JSON.stringify(contents));
+    xhr.send(payload);
 
     $(this).trigger('reload');
 });
+
+$('#manual-submission textarea').val(JSON.stringify({
+    'branch': 'webkit-trunk',
+    'platform': 'chromium-mac',
+    'builder-name': 'Chromium Mac Release (Perf)',
+    'build-number': '123',
+    'timestamp': parseInt(Date.now() / 1000),
+    'webkit-revision': 104856,
+    'chromium-revision': 123059,
+    'results':
+        {
+            'webkit_style_test': {'avg': 100, 'median': 102, 'stdev': 5, 'min': 90, 'max': 110},
+            'some_test': 54,
+        },
+}, null, '  '));
index ff32459..0d78453 100644 (file)
@@ -20,8 +20,9 @@ from google.appengine.ext.webapp import util
 
 import json
 
-from admin_handlers import IsAdminHandler
 from admin_handlers import AdminDashboardHandler
+from admin_handlers import IsAdminHandler
+from admin_handlers import MergeTestsHandler
 from controller import CachedDashboardHandler
 from controller import CachedManifestHandler
 from controller import CachedRunsHandler
@@ -33,11 +34,10 @@ from report_handler import ReportHandler
 from report_handler import AdminReportHandler
 from report_process_handler import ReportProcessHandler
 from report_logs_handler import ReportLogsHandler
-from merge_tests_handler import MergeTestsHandler
 
 routes = [
     ('/admin/report/?', AdminReportHandler),
-    ('/admin/merge-tests/?', MergeTestsHandler),
+    (r'/admin/merge-tests(?:/(.*))?', MergeTestsHandler),
     ('/admin/report-logs/?', ReportLogsHandler),
     ('/admin/create/(.*)', CreateHandler),
     (r'/admin/([A-Za-z\-]*)', AdminDashboardHandler),
diff --git a/Websites/webkit-perf.appspot.com/merge_tests_handler.py b/Websites/webkit-perf.appspot.com/merge_tests_handler.py
deleted file mode 100644 (file)
index 7ec19a8..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-#!/usr/bin/env python
-# Copyright (C) 2012 Google 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:
-#
-#     * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#     * 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.
-#     * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT
-# OWNER OR 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.
-
-import webapp2
-from google.appengine.ext.webapp import template
-
-import json
-import os
-
-from controller import schedule_runs_update
-from controller import schedule_dashboard_update
-from controller import schedule_manifest_update
-from models import Test
-from models import TestResult
-from models import delete_model_with_numeric_id_holder
-
-
-class MergeTestsHandler(webapp2.RequestHandler):
-    def post(self):
-        self.response.headers['Content-Type'] = 'text/plain; charset=utf-8';
-
-        try:
-            payload = json.loads(self.request.body)
-            merge = payload.get('merge', '')
-            into = payload.get('into', '')
-        except:
-            self.response.out.write("Failed to parse the payload: %s" % self.request.body)
-            return
-
-        merge = Test.get_by_key_name(merge)
-        into = Test.get_by_key_name(into)
-        if not merge or not into:
-            self.response.out.write('Invalid test names')
-            return
-
-        merged_results = TestResult.all()
-        merged_results.filter('name =', merge.name)
-        branches_and_platforms_to_update = set()
-        for result in merged_results:
-            branches_and_platforms_to_update.add((result.build.branch.id, result.build.platform.id))
-            result.name = into.name
-            result.put()
-
-        for branch_id, platform_id in branches_and_platforms_to_update:
-            schedule_runs_update(into.id, branch_id, platform_id)
-
-        schedule_dashboard_update()
-        schedule_manifest_update()
-
-        delete_model_with_numeric_id_holder(merge)
-
-        self.response.out.write('OK')
index ce3fe64..f9bb584 100644 (file)
@@ -142,6 +142,8 @@ class Build(db.Model):
 class Test(db.Model):
     id = db.IntegerProperty(required=True)
     name = db.StringProperty(required=True)
+    # FIXME: Storing branches and platforms separately is flawed since a test maybe available on
+    # one platform but only on some branch and vice versa.
     branches = db.ListProperty(db.Key)
     platforms = db.ListProperty(db.Key)
 
@@ -170,6 +172,26 @@ class Test(db.Model):
 
         return create_in_transaction_with_numeric_id_holder(execute) or existing_test[0]
 
+    def merge(self, other):
+        assert self.key() != other.key()
+
+        merged_results = TestResult.all()
+        merged_results.filter('name =', other.name)
+
+        # FIXME: We should be doing this check in a transaction but only ancestor queries are allowed
+        for result in merged_results:
+            if TestResult.get_by_key_name(TestResult.key_name(result.build, self.name)):
+                return None
+
+        branches_and_platforms_to_update = set()
+        for result in merged_results:
+            branches_and_platforms_to_update.add((result.build.branch.id, result.build.platform.id))
+            result.replace_to_change_test_name(self.name)
+
+        delete_model_with_numeric_id_holder(other)
+
+        return branches_and_platforms_to_update
+
 
 class TestResult(db.Model):
     name = db.StringProperty(required=True)
@@ -201,6 +223,13 @@ class TestResult(db.Model):
             valueMedian=_float_or_none(result, 'median'), valueStdev=_float_or_none(result, 'stdev'),
             valueMin=_float_or_none(result, 'min'), valueMax=_float_or_none(result, 'max'))
 
+    def replace_to_change_test_name(self, new_name):
+        clone = TestResult(key_name=TestResult.key_name(self.build, new_name), name=new_name, build=self.build,
+            value=self.value, valueMedian=self.valueMedian, valueStdev=self.valueMin, valueMin=self.valueMin, valueMax=self.valueMax)
+        clone.put()
+        self.delete()
+        return clone
+
 
 class ReportLog(db.Model):
     timestamp = db.DateTimeProperty(required=True)
index 9c09105..4838f13 100644 (file)
@@ -49,11 +49,18 @@ class DataStoreTestsBase(unittest.TestCase):
         self.assertEqual(len(model.all().fetch(5)), 0)
 
     def assertOnlyInstance(self, only_instasnce):
-        self.assertEqual(len(only_instasnce.__class__.all().fetch(5)), 1)
-        self.assertTrue(only_instasnce.__class__.get(only_instasnce.key()))
+        self.assertOnlyInstances([only_instasnce])
+
+    def assertOnlyInstances(self, expected_instances):
+        actual_instances = expected_instances[0].__class__.all().fetch(len(expected_instances) + 1)
+        self.assertEqualUnorderedModelList(actual_instances, expected_instances)
+
+    def assertEqualUnorderedModelList(self, list1, list2):
+        self.assertEqualUnorderedList([item.key() for item in list1], [item.key() for item in list1])
 
     def assertEqualUnorderedList(self, list1, list2):
         self.assertEqual(set(list1), set(list2))
+        self.assertEqual(len(list1), len(list2))
 
 
 class HelperTests(DataStoreTestsBase):
@@ -200,6 +207,12 @@ def _create_some_builder():
     return branch, platform, models.Builder.get(builder_key)
 
 
+def _create_build(branch, platform, builder, key_name='some-build'):
+    build_key = models.Build(key_name=key_name, branch=branch, platform=platform, builder=builder,
+        buildNumber=1, revision=100, timestamp=datetime.now()).put()
+    return models.Build.get(build_key)
+
+
 class BuildTests(DataStoreTestsBase):
     def test_get_or_insert_from_log(self):
         branch, platform, builder = _create_some_builder()
@@ -254,16 +267,42 @@ class TestModelTests(DataStoreTestsBase):
         self.assertEqualUnorderedList(test.branches, [branch.key(), other_branch.key()])
         self.assertEqualUnorderedList(test.platforms, [platform.key(), other_platform.key()])
 
-
-class TestResultTests(DataStoreTestsBase):
-    def _create_build(self):
+    def test_merge(self):
         branch, platform, builder = _create_some_builder()
-        build_key = models.Build(key_name='some-build', branch=branch, platform=platform, builder=builder,
-            buildNumber=1, revision=100, timestamp=datetime.now()).put()
-        return models.Build.get(build_key)
+        some_build = _create_build(branch, platform, builder)
+        some_result = models.TestResult.get_or_insert_from_parsed_json('some-test', some_build, 50)
+        some_test = models.Test.update_or_insert('some-test', branch, platform)
+
+        other_build = _create_build(branch, platform, builder, 'other-build')
+        other_result = models.TestResult.get_or_insert_from_parsed_json('other-test', other_build, 30)
+        other_test = models.Test.update_or_insert('other-test', branch, platform)
+
+        self.assertOnlyInstances([some_result, other_result])
+        self.assertNotEqual(some_result.key(), other_result.key())
+        self.assertOnlyInstances([some_test, other_test])
+
+        self.assertRaises(AssertionError, some_test.merge, (some_test))
+        self.assertOnlyInstances([some_test, other_test])
+
+        some_test.merge(other_test)
+        results_for_some_test = models.TestResult.all()
+        results_for_some_test.filter('name =', 'some-test')
+        results_for_some_test = results_for_some_test.fetch(5)
+        self.assertEqual(len(results_for_some_test), 2)
+
+        self.assertEqual(results_for_some_test[0].name, 'some-test')
+        self.assertEqual(results_for_some_test[1].name, 'some-test')
+
+        if results_for_some_test[0].value == 50:
+            self.assertEqual(results_for_some_test[1].value, 30)
+        else:
+            self.assertEqual(results_for_some_test[1].value, 50)
 
+
+class TestResultTests(DataStoreTestsBase):
     def test_get_or_insert_value(self):
-        build = self._create_build()
+        branch, platform, builder = _create_some_builder()
+        build = _create_build(branch, platform, builder)
         self.assertThereIsNoInstanceOf(models.TestResult)
         result = models.TestResult.get_or_insert_from_parsed_json('some-test', build, 50)
         self.assertOnlyInstance(result)
@@ -276,7 +315,8 @@ class TestResultTests(DataStoreTestsBase):
         self.assertEqual(result.valueMax, None)
 
     def test_get_or_insert_stat_value(self):
-        build = self._create_build()
+        branch, platform, builder = _create_some_builder()
+        build = _create_build(branch, platform, builder)
         self.assertThereIsNoInstanceOf(models.TestResult)
         result = models.TestResult.get_or_insert_from_parsed_json('some-test', build,
             {"avg": 40, "median": "40.1", "stdev": 3.25, "min": 30.5, "max": 45})
@@ -289,6 +329,64 @@ class TestResultTests(DataStoreTestsBase):
         self.assertEqual(result.valueMin, 30.5)
         self.assertEqual(result.valueMax, 45)
 
+    def test_replace_to_change_test_name(self):
+        branch, platform, builder = _create_some_builder()
+        build = _create_build(branch, platform, builder)
+        self.assertThereIsNoInstanceOf(models.TestResult)
+        result = models.TestResult.get_or_insert_from_parsed_json('some-test', build, 50)
+        self.assertOnlyInstance(result)
+        self.assertEqual(result.name, 'some-test')
+
+        new_result = result.replace_to_change_test_name('other-test')
+        self.assertNotEqual(result, new_result)
+        self.assertOnlyInstance(new_result)
+
+        self.assertEqual(new_result.name, 'other-test')
+        self.assertEqual(new_result.build.key(), result.build.key())
+        self.assertEqual(new_result.value, result.value)
+        self.assertEqual(new_result.valueMedian, None)
+        self.assertEqual(new_result.valueStdev, None)
+        self.assertEqual(new_result.valueMin, None)
+        self.assertEqual(new_result.valueMax, None)
+
+    def test_replace_to_change_test_name_with_stat_value(self):
+        branch, platform, builder = _create_some_builder()
+        build = _create_build(branch, platform, builder)
+        self.assertThereIsNoInstanceOf(models.TestResult)
+        result = models.TestResult.get_or_insert_from_parsed_json('some-test', build,
+            {"avg": 40, "median": "40.1", "stdev": 3.25, "min": 30.5, "max": 45})
+        self.assertOnlyInstance(result)
+        self.assertEqual(result.name, 'some-test')
+
+        new_result = result.replace_to_change_test_name('other-test')
+        self.assertNotEqual(result, new_result)
+        self.assertOnlyInstance(new_result)
+
+        self.assertEqual(new_result.name, 'other-test')
+        self.assertEqual(new_result.build.key(), result.build.key())
+        self.assertEqual(new_result.value, result.value)
+        self.assertEqual(result.value, 40.0)
+        self.assertEqual(result.valueMedian, 40.1)
+        self.assertEqual(result.valueStdev, 3.25)
+        self.assertEqual(result.valueMin, 30.5)
+        self.assertEqual(result.valueMax, 45)
+
+    def test_replace_to_change_test_name_overrides_conflicting_result(self):
+        branch, platform, builder = _create_some_builder()
+        build = _create_build(branch, platform, builder)
+        self.assertThereIsNoInstanceOf(models.TestResult)
+        result = models.TestResult.get_or_insert_from_parsed_json('some-test', build, 20)
+        self.assertOnlyInstance(result)
+
+        conflicting_result = models.TestResult.get_or_insert_from_parsed_json('other-test', build, 10)
+
+        new_result = result.replace_to_change_test_name('other-test')
+        self.assertNotEqual(result, new_result)
+        self.assertOnlyInstance(new_result)
+
+        self.assertEqual(new_result.name, 'other-test')
+        self.assertEqual(models.TestResult.get(conflicting_result.key()).value, 20)
+
 
 class ReportLogTests(DataStoreTestsBase):
     def _create_log_with_payload(self, payload):