2011-03-30 Maciej Stachowiak <mjs@apple.com>
[WebKit.git] / Tools / Scripts / webkitpy / tool / mocktool.py
1 # Copyright (C) 2009 Google Inc. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions are
5 # met:
6 #
7 #    * Redistributions of source code must retain the above copyright
8 # notice, this list of conditions and the following disclaimer.
9 #    * Redistributions in binary form must reproduce the above
10 # copyright notice, this list of conditions and the following disclaimer
11 # in the documentation and/or other materials provided with the
12 # distribution.
13 #    * Neither the name of Google Inc. nor the names of its
14 # contributors may be used to endorse or promote products derived from
15 # this software without specific prior written permission.
16 #
17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29 import os
30 import threading
31
32 from webkitpy.common.config.committers import CommitterList, Reviewer
33 from webkitpy.common.checkout.commitinfo import CommitInfo
34 from webkitpy.common.checkout.scm import CommitMessage
35 from webkitpy.common.net.bugzilla import Bug, Attachment
36 from webkitpy.common.system.deprecated_logging import log
37 from webkitpy.common.system.filesystem_mock import MockFileSystem
38 from webkitpy.thirdparty.mock import Mock
39
40
41 def _id_to_object_dictionary(*objects):
42     dictionary = {}
43     for thing in objects:
44         dictionary[thing["id"]] = thing
45     return dictionary
46
47 # Testing
48
49 # FIXME: The ids should be 1, 2, 3 instead of crazy numbers.
50
51
52 _patch1 = {
53     "id": 197,
54     "bug_id": 42,
55     "url": "http://example.com/197",
56     "name": "Patch1",
57     "is_obsolete": False,
58     "is_patch": True,
59     "review": "+",
60     "reviewer_email": "foo@bar.com",
61     "commit-queue": "+",
62     "committer_email": "foo@bar.com",
63     "attacher_email": "Contributer1",
64 }
65
66
67 _patch2 = {
68     "id": 128,
69     "bug_id": 42,
70     "url": "http://example.com/128",
71     "name": "Patch2",
72     "is_obsolete": False,
73     "is_patch": True,
74     "review": "+",
75     "reviewer_email": "foo@bar.com",
76     "commit-queue": "+",
77     "committer_email": "non-committer@example.com",
78     "attacher_email": "eric@webkit.org",
79 }
80
81
82 _patch3 = {
83     "id": 103,
84     "bug_id": 75,
85     "url": "http://example.com/103",
86     "name": "Patch3",
87     "is_obsolete": False,
88     "is_patch": True,
89     "review": "?",
90     "attacher_email": "eric@webkit.org",
91 }
92
93
94 _patch4 = {
95     "id": 104,
96     "bug_id": 77,
97     "url": "http://example.com/103",
98     "name": "Patch3",
99     "is_obsolete": False,
100     "is_patch": True,
101     "review": "+",
102     "commit-queue": "?",
103     "reviewer_email": "foo@bar.com",
104     "attacher_email": "Contributer2",
105 }
106
107
108 _patch5 = {
109     "id": 105,
110     "bug_id": 77,
111     "url": "http://example.com/103",
112     "name": "Patch5",
113     "is_obsolete": False,
114     "is_patch": True,
115     "review": "+",
116     "reviewer_email": "foo@bar.com",
117     "attacher_email": "eric@webkit.org",
118 }
119
120
121 _patch6 = { # Valid committer, but no reviewer.
122     "id": 106,
123     "bug_id": 77,
124     "url": "http://example.com/103",
125     "name": "ROLLOUT of r3489",
126     "is_obsolete": False,
127     "is_patch": True,
128     "commit-queue": "+",
129     "committer_email": "foo@bar.com",
130     "attacher_email": "eric@webkit.org",
131 }
132
133
134 _patch7 = { # Valid review, patch is marked obsolete.
135     "id": 107,
136     "bug_id": 76,
137     "url": "http://example.com/103",
138     "name": "Patch7",
139     "is_obsolete": True,
140     "is_patch": True,
141     "review": "+",
142     "reviewer_email": "foo@bar.com",
143     "attacher_email": "eric@webkit.org",
144 }
145
146
147 # This matches one of Bug.unassigned_emails
148 _unassigned_email = "webkit-unassigned@lists.webkit.org"
149 # This is needed for the FlakyTestReporter to believe the bug
150 # was filed by one of the webkitpy bots.
151 _commit_queue_email = "commit-queue@webkit.org"
152
153
154 # FIXME: The ids should be 1, 2, 3 instead of crazy numbers.
155
156
157 _bug1 = {
158     "id": 42,
159     "title": "Bug with two r+'d and cq+'d patches, one of which has an "
160              "invalid commit-queue setter.",
161     "reporter_email": "foo@foo.com",
162     "assigned_to_email": _unassigned_email,
163     "attachments": [_patch1, _patch2],
164     "bug_status": "UNCONFIRMED",
165 }
166
167
168 _bug2 = {
169     "id": 75,
170     "title": "Bug with a patch needing review.",
171     "reporter_email": "foo@foo.com",
172     "assigned_to_email": "foo@foo.com",
173     "attachments": [_patch3],
174     "bug_status": "ASSIGNED",
175 }
176
177
178 _bug3 = {
179     "id": 76,
180     "title": "The third bug",
181     "reporter_email": "foo@foo.com",
182     "assigned_to_email": _unassigned_email,
183     "attachments": [_patch7],
184     "bug_status": "NEW",
185 }
186
187
188 _bug4 = {
189     "id": 77,
190     "title": "The fourth bug",
191     "reporter_email": "foo@foo.com",
192     "assigned_to_email": "foo@foo.com",
193     "attachments": [_patch4, _patch5, _patch6],
194     "bug_status": "REOPENED",
195 }
196
197
198 _bug5 = {
199     "id": 78,
200     "title": "The fifth bug",
201     "reporter_email": _commit_queue_email,
202     "assigned_to_email": "foo@foo.com",
203     "attachments": [],
204     "bug_status": "RESOLVED",
205     "dup_id": 76,
206 }
207
208
209 # FIXME: This should not inherit from Mock
210 class MockBugzillaQueries(Mock):
211
212     def __init__(self, bugzilla):
213         Mock.__init__(self)
214         self._bugzilla = bugzilla
215
216     def _all_bugs(self):
217         return map(lambda bug_dictionary: Bug(bug_dictionary, self._bugzilla),
218                    self._bugzilla.bug_cache.values())
219
220     def fetch_bug_ids_from_commit_queue(self):
221         bugs_with_commit_queued_patches = filter(
222                 lambda bug: bug.commit_queued_patches(),
223                 self._all_bugs())
224         return map(lambda bug: bug.id(), bugs_with_commit_queued_patches)
225
226     def fetch_attachment_ids_from_review_queue(self):
227         unreviewed_patches = sum([bug.unreviewed_patches()
228                                   for bug in self._all_bugs()], [])
229         return map(lambda patch: patch.id(), unreviewed_patches)
230
231     def fetch_patches_from_commit_queue(self):
232         return sum([bug.commit_queued_patches()
233                     for bug in self._all_bugs()], [])
234
235     def fetch_bug_ids_from_pending_commit_list(self):
236         bugs_with_reviewed_patches = filter(lambda bug: bug.reviewed_patches(),
237                                             self._all_bugs())
238         bug_ids = map(lambda bug: bug.id(), bugs_with_reviewed_patches)
239         # NOTE: This manual hack here is to allow testing logging in
240         # test_assign_to_committer the real pending-commit query on bugzilla
241         # will return bugs with patches which have r+, but are also obsolete.
242         return bug_ids + [76]
243
244     def fetch_patches_from_pending_commit_list(self):
245         return sum([bug.reviewed_patches() for bug in self._all_bugs()], [])
246
247     def fetch_bugs_matching_search(self, search_string, author_email=None):
248         return [self._bugzilla.fetch_bug(78), self._bugzilla.fetch_bug(77)]
249
250 _mock_reviewer = Reviewer("Foo Bar", "foo@bar.com")
251
252
253 # FIXME: Bugzilla is the wrong Mock-point.  Once we have a BugzillaNetwork
254 #        class we should mock that instead.
255 # Most of this class is just copy/paste from Bugzilla.
256 # FIXME: This should not inherit from Mock
257 class MockBugzilla(Mock):
258
259     bug_server_url = "http://example.com"
260
261     bug_cache = _id_to_object_dictionary(_bug1, _bug2, _bug3, _bug4, _bug5)
262
263     attachment_cache = _id_to_object_dictionary(_patch1,
264                                                 _patch2,
265                                                 _patch3,
266                                                 _patch4,
267                                                 _patch5,
268                                                 _patch6,
269                                                 _patch7)
270
271     def __init__(self):
272         Mock.__init__(self)
273         self.queries = MockBugzillaQueries(self)
274         self.committers = CommitterList(reviewers=[_mock_reviewer])
275         self._override_patch = None
276
277     def create_bug(self,
278                    bug_title,
279                    bug_description,
280                    component=None,
281                    diff=None,
282                    patch_description=None,
283                    cc=None,
284                    blocked=None,
285                    mark_for_review=False,
286                    mark_for_commit_queue=False):
287         log("MOCK create_bug")
288         log("bug_title: %s" % bug_title)
289         log("bug_description: %s" % bug_description)
290         if component:
291             log("component: %s" % component)
292         if cc:
293             log("cc: %s" % cc)
294         if blocked:
295             log("blocked: %s" % blocked)
296         return 78
297
298     def quips(self):
299         return ["Good artists copy. Great artists steal. - Pablo Picasso"]
300
301     def fetch_bug(self, bug_id):
302         return Bug(self.bug_cache.get(bug_id), self)
303
304     def set_override_patch(self, patch):
305         self._override_patch = patch
306
307     def fetch_attachment(self, attachment_id):
308         if self._override_patch:
309             return self._override_patch
310
311         attachment_dictionary = self.attachment_cache.get(attachment_id)
312         if not attachment_dictionary:
313             print "MOCK: fetch_attachment: %s is not a known attachment id" % attachment_id
314             return None
315         bug = self.fetch_bug(attachment_dictionary["bug_id"])
316         for attachment in bug.attachments(include_obsolete=True):
317             if attachment.id() == int(attachment_id):
318                 return attachment
319
320     def bug_url_for_bug_id(self, bug_id):
321         return "%s/%s" % (self.bug_server_url, bug_id)
322
323     def fetch_bug_dictionary(self, bug_id):
324         return self.bug_cache.get(bug_id)
325
326     def attachment_url_for_id(self, attachment_id, action="view"):
327         action_param = ""
328         if action and action != "view":
329             action_param = "&action=%s" % action
330         return "%s/%s%s" % (self.bug_server_url, attachment_id, action_param)
331
332     def set_flag_on_attachment(self,
333                                attachment_id,
334                                flag_name,
335                                flag_value,
336                                comment_text=None,
337                                additional_comment_text=None):
338         log("MOCK setting flag '%s' to '%s' on attachment '%s' with comment '%s' and additional comment '%s'" % (
339             flag_name, flag_value, attachment_id, comment_text, additional_comment_text))
340
341     def post_comment_to_bug(self, bug_id, comment_text, cc=None):
342         log("MOCK bug comment: bug_id=%s, cc=%s\n--- Begin comment ---\n%s\n--- End comment ---\n" % (
343             bug_id, cc, comment_text))
344
345     def add_attachment_to_bug(self,
346                               bug_id,
347                               file_or_string,
348                               description,
349                               filename=None,
350                               comment_text=None):
351         log("MOCK add_attachment_to_bug: bug_id=%s, description=%s filename=%s" % (bug_id, description, filename))
352         if comment_text:
353             log("-- Begin comment --")
354             log(comment_text)
355             log("-- End comment --")
356
357     def add_patch_to_bug(self,
358                          bug_id,
359                          diff,
360                          description,
361                          comment_text=None,
362                          mark_for_review=False,
363                          mark_for_commit_queue=False,
364                          mark_for_landing=False):
365         log("MOCK add_patch_to_bug: bug_id=%s, description=%s, mark_for_review=%s, mark_for_commit_queue=%s, mark_for_landing=%s" %
366             (bug_id, description, mark_for_review, mark_for_commit_queue, mark_for_landing))
367         if comment_text:
368             log("-- Begin comment --")
369             log(comment_text)
370             log("-- End comment --")
371
372
373 class MockBuilder(object):
374     def __init__(self, name):
375         self._name = name
376
377     def name(self):
378         return self._name
379
380     def results_url(self):
381         return "http://example.com/builders/%s/results/" % self.name()
382
383     def force_build(self, username, comments):
384         log("MOCK: force_build: name=%s, username=%s, comments=%s" % (
385             self._name, username, comments))
386
387
388 class MockFailureMap(object):
389     def __init__(self, buildbot):
390         self._buildbot = buildbot
391
392     def is_empty(self):
393         return False
394
395     def filter_out_old_failures(self, is_old_revision):
396         pass
397
398     def failing_revisions(self):
399         return [29837]
400
401     def builders_failing_for(self, revision):
402         return [self._buildbot.builder_with_name("Builder1")]
403
404     def tests_failing_for(self, revision):
405         return ["mock-test-1"]
406
407
408 class MockBuildBot(object):
409     buildbot_host = "dummy_buildbot_host"
410     def __init__(self):
411         self._mock_builder1_status = {
412             "name": "Builder1",
413             "is_green": True,
414             "activity": "building",
415         }
416         self._mock_builder2_status = {
417             "name": "Builder2",
418             "is_green": True,
419             "activity": "idle",
420         }
421
422     def builder_with_name(self, name):
423         return MockBuilder(name)
424
425     def builder_statuses(self):
426         return [
427             self._mock_builder1_status,
428             self._mock_builder2_status,
429         ]
430
431     def red_core_builders_names(self):
432         if not self._mock_builder2_status["is_green"]:
433             return [self._mock_builder2_status["name"]]
434         return []
435
436     def red_core_builders(self):
437         if not self._mock_builder2_status["is_green"]:
438             return [self._mock_builder2_status]
439         return []
440
441     def idle_red_core_builders(self):
442         if not self._mock_builder2_status["is_green"]:
443             return [self._mock_builder2_status]
444         return []
445
446     def last_green_revision(self):
447         return 9479
448
449     def light_tree_on_fire(self):
450         self._mock_builder2_status["is_green"] = False
451
452     def failure_map(self):
453         return MockFailureMap(self)
454
455
456 # FIXME: This should not inherit from Mock
457 class MockSCM(Mock):
458
459     fake_checkout_root = os.path.realpath("/tmp") # realpath is needed to allow for Mac OS X's /private/tmp
460
461     def __init__(self):
462         Mock.__init__(self)
463         # FIXME: We should probably use real checkout-root detection logic here.
464         # os.getcwd() can't work here because other parts of the code assume that "checkout_root"
465         # will actually be the root.  Since getcwd() is wrong, use a globally fake root for now.
466         self.checkout_root = self.fake_checkout_root
467         self.added_paths = set()
468
469     def add(self, destination_path, return_exit_code=False):
470         self.added_paths.add(destination_path)
471         if return_exit_code:
472             return 0
473
474     def changed_files(self, git_commit=None):
475         return ["MockFile1"]
476
477     def create_patch(self, git_commit, changed_files=None):
478         return "Patch1"
479
480     def commit_ids_from_commitish_arguments(self, args):
481         return ["Commitish1", "Commitish2"]
482
483     def commit_message_for_local_commit(self, commit_id):
484         if commit_id == "Commitish1":
485             return CommitMessage("CommitMessage1\n" \
486                 "https://bugs.example.org/show_bug.cgi?id=42\n")
487         if commit_id == "Commitish2":
488             return CommitMessage("CommitMessage2\n" \
489                 "https://bugs.example.org/show_bug.cgi?id=75\n")
490         raise Exception("Bogus commit_id in commit_message_for_local_commit.")
491
492     def diff_for_file(self, path, log=None):
493         return path + '-diff'
494
495     def diff_for_revision(self, revision):
496         return "DiffForRevision%s\n" \
497                "http://bugs.webkit.org/show_bug.cgi?id=12345" % revision
498
499     def show_head(self, path):
500         return path
501
502     def svn_revision_from_commit_text(self, commit_text):
503         return "49824"
504
505
506 class MockDEPS(object):
507     def read_variable(self, name):
508         return 6564
509
510     def write_variable(self, name, value):
511         log("MOCK: MockDEPS.write_variable(%s, %s)" % (name, value))
512
513
514 class MockCheckout(object):
515
516     _committer_list = CommitterList()
517
518     def commit_info_for_revision(self, svn_revision):
519         # The real Checkout would probably throw an exception, but this is the only way tests have to get None back at the moment.
520         if not svn_revision:
521             return None
522         return CommitInfo(svn_revision, "eric@webkit.org", {
523             "bug_id": 42,
524             "author_name": "Adam Barth",
525             "author_email": "abarth@webkit.org",
526             "author": self._committer_list.committer_by_email("abarth@webkit.org"),
527             "reviewer_text": "Darin Adler",
528             "reviewer": self._committer_list.committer_by_name("Darin Adler"),
529         })
530
531     def bug_id_for_revision(self, svn_revision):
532         return 12345
533
534     def recent_commit_infos_for_files(self, paths):
535         return [self.commit_info_for_revision(32)]
536
537     def modified_changelogs(self, git_commit, changed_files=None):
538         # Ideally we'd return something more interesting here.  The problem is
539         # that LandDiff will try to actually read the patch from disk!
540         return []
541
542     def commit_message_for_this_commit(self, git_commit, changed_files=None):
543         commit_message = Mock()
544         commit_message.message = lambda:"This is a fake commit message that is at least 50 characters."
545         return commit_message
546
547     def chromium_deps(self):
548         return MockDEPS()
549
550     def apply_patch(self, patch, force=False):
551         pass
552
553     def apply_reverse_diffs(self, revision):
554         pass
555
556     def suggested_reviewers(self, git_commit, changed_files=None):
557         return [_mock_reviewer]
558
559
560 class MockUser(object):
561
562     @classmethod
563     def prompt(cls, message, repeat=1, raw_input=raw_input):
564         return "Mock user response"
565
566     @classmethod
567     def prompt_with_list(cls, list_title, list_items, can_choose_multiple=False, raw_input=raw_input):
568         pass
569
570     def __init__(self):
571         self.opened_urls = []
572
573     def edit(self, files):
574         pass
575
576     def edit_changelog(self, files):
577         pass
578
579     def page(self, message):
580         pass
581
582     def confirm(self, message=None, default='y'):
583         log(message)
584         return default == 'y'
585
586     def can_open_url(self):
587         return True
588
589     def open_url(self, url):
590         self.opened_urls.append(url)
591         if url.startswith("file://"):
592             log("MOCK: user.open_url: file://...")
593             return
594         log("MOCK: user.open_url: %s" % url)
595
596
597 class MockIRC(object):
598
599     def post(self, message):
600         log("MOCK: irc.post: %s" % message)
601
602     def disconnect(self):
603         log("MOCK: irc.disconnect")
604
605
606 class MockStatusServer(object):
607
608     def __init__(self, bot_id=None, work_items=None):
609         self.host = "example.com"
610         self.bot_id = bot_id
611         self._work_items = work_items or []
612
613     def patch_status(self, queue_name, patch_id):
614         return None
615
616     def svn_revision(self, svn_revision):
617         return None
618
619     def next_work_item(self, queue_name):
620         if not self._work_items:
621             return None
622         return self._work_items.pop(0)
623
624     def release_work_item(self, queue_name, patch):
625         log("MOCK: release_work_item: %s %s" % (queue_name, patch.id()))
626
627     def update_work_items(self, queue_name, work_items):
628         self._work_items = work_items
629         log("MOCK: update_work_items: %s %s" % (queue_name, work_items))
630
631     def submit_to_ews(self, patch_id):
632         log("MOCK: submit_to_ews: %s" % (patch_id))
633
634     def update_status(self, queue_name, status, patch=None, results_file=None):
635         log("MOCK: update_status: %s %s" % (queue_name, status))
636         return 187
637
638     def update_svn_revision(self, svn_revision, broken_bot):
639         return 191
640
641     def results_url_for_status(self, status_id):
642         return "http://dummy_url"
643
644
645 # FIXME: This should not inherit from Mock
646 # FIXME: Unify with common.system.executive_mock.MockExecutive.
647 class MockExecutive(Mock):
648     def __init__(self, should_log):
649         self.should_log = should_log
650
651     def run_and_throw_if_fail(self, args, quiet=False):
652         if self.should_log:
653             log("MOCK run_and_throw_if_fail: %s" % args)
654         return "MOCK output of child process"
655
656     def run_command(self,
657                     args,
658                     cwd=None,
659                     input=None,
660                     error_handler=None,
661                     return_exit_code=False,
662                     return_stderr=True,
663                     decode_output=False):
664         if self.should_log:
665             log("MOCK run_command: %s" % args)
666         return "MOCK output of child process"
667
668
669 class MockOptions(object):
670     """Mock implementation of optparse.Values."""
671
672     def __init__(self, **kwargs):
673         # The caller can set option values using keyword arguments. We don't
674         # set any values by default because we don't know how this
675         # object will be used. Generally speaking unit tests should
676         # subclass this or provider wrapper functions that set a common
677         # set of options.
678         for key, value in kwargs.items():
679             self.__dict__[key] = value
680
681
682 class MockPort(Mock):
683     def name(self):
684         return "MockPort"
685
686     def layout_tests_results_path(self):
687         return "/mock/results.html"
688
689 class MockTestPort1(object):
690
691     def skips_layout_test(self, test_name):
692         return test_name in ["media/foo/bar.html", "foo"]
693
694
695 class MockTestPort2(object):
696
697     def skips_layout_test(self, test_name):
698         return test_name == "media/foo/bar.html"
699
700
701 class MockPortFactory(object):
702
703     def get_all(self, options=None):
704         return {"test_port1": MockTestPort1(), "test_port2": MockTestPort2()}
705
706
707 class MockPlatformInfo(object):
708     def display_name(self):
709         return "MockPlatform 1.0"
710
711
712 class MockWorkspace(object):
713     def find_unused_filename(self, directory, name, extension, search_limit=10):
714         return "%s/%s.%s" % (directory, name, extension)
715
716     def create_zip(self, zip_path, source_path):
717         pass
718
719
720 class MockTool(object):
721
722     def __init__(self, log_executive=False):
723         self.wakeup_event = threading.Event()
724         self.bugs = MockBugzilla()
725         self.buildbot = MockBuildBot()
726         self.executive = MockExecutive(should_log=log_executive)
727         self.filesystem = MockFileSystem()
728         self.workspace = MockWorkspace()
729         self._irc = None
730         self.user = MockUser()
731         self._scm = MockSCM()
732         self._checkout = MockCheckout()
733         self.status_server = MockStatusServer()
734         self.irc_password = "MOCK irc password"
735         self.port_factory = MockPortFactory()
736         self.platform = MockPlatformInfo()
737
738     def scm(self):
739         return self._scm
740
741     def checkout(self):
742         return self._checkout
743
744     def ensure_irc_connected(self, delegate):
745         if not self._irc:
746             self._irc = MockIRC()
747
748     def irc(self):
749         return self._irc
750
751     def path(self):
752         return "echo"
753
754     def port(self):
755         return MockPort()
756
757
758 class MockBrowser(object):
759     params = {}
760
761     def open(self, url):
762         pass
763
764     def select_form(self, name):
765         pass
766
767     def __setitem__(self, key, value):
768         self.params[key] = value
769
770     def submit(self):
771         return Mock(file)