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
29import os
30import threading
31
32from webkitpy.common.config.committers import CommitterList, Reviewer
33from webkitpy.common.checkout.commitinfo import CommitInfo
34from webkitpy.common.checkout.scm import CommitMessage
35from webkitpy.common.net.bugzilla import Bug, Attachment
36from webkitpy.common.system.deprecated_logging import log
37from webkitpy.common.system.filesystem_mock import MockFileSystem
38from webkitpy.thirdparty.mock import Mock
39
40
41def _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
210class 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
257class 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
373class 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
388class 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
408class 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
457class 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, filesystem=None):
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        self._filesystem = filesystem
469
470    def add(self, destination_path, return_exit_code=False):
471        self.added_paths.add(destination_path)
472        if return_exit_code:
473            return 0
474
475    def changed_files(self, git_commit=None):
476        return ["MockFile1"]
477
478    def create_patch(self, git_commit, changed_files=None):
479        return "Patch1"
480
481    def commit_ids_from_commitish_arguments(self, args):
482        return ["Commitish1", "Commitish2"]
483
484    def commit_message_for_local_commit(self, commit_id):
485        if commit_id == "Commitish1":
486            return CommitMessage("CommitMessage1\n" \
487                "https://bugs.example.org/show_bug.cgi?id=42\n")
488        if commit_id == "Commitish2":
489            return CommitMessage("CommitMessage2\n" \
490                "https://bugs.example.org/show_bug.cgi?id=75\n")
491        raise Exception("Bogus commit_id in commit_message_for_local_commit.")
492
493    def diff_for_file(self, path, log=None):
494        return path + '-diff'
495
496    def diff_for_revision(self, revision):
497        return "DiffForRevision%s\n" \
498               "http://bugs.webkit.org/show_bug.cgi?id=12345" % revision
499
500    def show_head(self, path):
501        return path
502
503    def svn_revision_from_commit_text(self, commit_text):
504        return "49824"
505
506    def delete(self, path):
507        if not self._filesystem:
508            return
509        if self._filesystem.exists(path):
510            self._filesystem.remove(path)
511
512
513class MockDEPS(object):
514    def read_variable(self, name):
515        return 6564
516
517    def write_variable(self, name, value):
518        log("MOCK: MockDEPS.write_variable(%s, %s)" % (name, value))
519
520
521class MockCheckout(object):
522
523    _committer_list = CommitterList()
524
525    def commit_info_for_revision(self, svn_revision):
526        # The real Checkout would probably throw an exception, but this is the only way tests have to get None back at the moment.
527        if not svn_revision:
528            return None
529        return CommitInfo(svn_revision, "eric@webkit.org", {
530            "bug_id": 42,
531            "author_name": "Adam Barth",
532            "author_email": "abarth@webkit.org",
533            "author": self._committer_list.committer_by_email("abarth@webkit.org"),
534            "reviewer_text": "Darin Adler",
535            "reviewer": self._committer_list.committer_by_name("Darin Adler"),
536        })
537
538    def bug_id_for_revision(self, svn_revision):
539        return 12345
540
541    def recent_commit_infos_for_files(self, paths):
542        return [self.commit_info_for_revision(32)]
543
544    def modified_changelogs(self, git_commit, changed_files=None):
545        # Ideally we'd return something more interesting here.  The problem is
546        # that LandDiff will try to actually read the patch from disk!
547        return []
548
549    def commit_message_for_this_commit(self, git_commit, changed_files=None):
550        commit_message = Mock()
551        commit_message.message = lambda:"This is a fake commit message that is at least 50 characters."
552        return commit_message
553
554    def chromium_deps(self):
555        return MockDEPS()
556
557    def apply_patch(self, patch, force=False):
558        pass
559
560    def apply_reverse_diffs(self, revision):
561        pass
562
563    def suggested_reviewers(self, git_commit, changed_files=None):
564        return [_mock_reviewer]
565
566
567class MockUser(object):
568
569    @classmethod
570    def prompt(cls, message, repeat=1, raw_input=raw_input):
571        return "Mock user response"
572
573    @classmethod
574    def prompt_with_list(cls, list_title, list_items, can_choose_multiple=False, raw_input=raw_input):
575        pass
576
577    def __init__(self):
578        self.opened_urls = []
579
580    def edit(self, files):
581        pass
582
583    def edit_changelog(self, files):
584        pass
585
586    def page(self, message):
587        pass
588
589    def confirm(self, message=None, default='y'):
590        log(message)
591        return default == 'y'
592
593    def can_open_url(self):
594        return True
595
596    def open_url(self, url):
597        self.opened_urls.append(url)
598        if url.startswith("file://"):
599            log("MOCK: user.open_url: file://...")
600            return
601        log("MOCK: user.open_url: %s" % url)
602
603
604class MockIRC(object):
605
606    def post(self, message):
607        log("MOCK: irc.post: %s" % message)
608
609    def disconnect(self):
610        log("MOCK: irc.disconnect")
611
612
613class MockStatusServer(object):
614
615    def __init__(self, bot_id=None, work_items=None):
616        self.host = "example.com"
617        self.bot_id = bot_id
618        self._work_items = work_items or []
619
620    def patch_status(self, queue_name, patch_id):
621        return None
622
623    def svn_revision(self, svn_revision):
624        return None
625
626    def next_work_item(self, queue_name):
627        if not self._work_items:
628            return None
629        return self._work_items.pop(0)
630
631    def release_work_item(self, queue_name, patch):
632        log("MOCK: release_work_item: %s %s" % (queue_name, patch.id()))
633
634    def update_work_items(self, queue_name, work_items):
635        self._work_items = work_items
636        log("MOCK: update_work_items: %s %s" % (queue_name, work_items))
637
638    def submit_to_ews(self, patch_id):
639        log("MOCK: submit_to_ews: %s" % (patch_id))
640
641    def update_status(self, queue_name, status, patch=None, results_file=None):
642        log("MOCK: update_status: %s %s" % (queue_name, status))
643        return 187
644
645    def update_svn_revision(self, svn_revision, broken_bot):
646        return 191
647
648    def results_url_for_status(self, status_id):
649        return "http://dummy_url"
650
651
652# FIXME: This should not inherit from Mock
653# FIXME: Unify with common.system.executive_mock.MockExecutive.
654class MockExecutive(Mock):
655    def __init__(self, should_log):
656        self.should_log = should_log
657
658    def run_and_throw_if_fail(self, args, quiet=False):
659        if self.should_log:
660            log("MOCK run_and_throw_if_fail: %s" % args)
661        return "MOCK output of child process"
662
663    def run_command(self,
664                    args,
665                    cwd=None,
666                    input=None,
667                    error_handler=None,
668                    return_exit_code=False,
669                    return_stderr=True,
670                    decode_output=False):
671        if self.should_log:
672            log("MOCK run_command: %s" % args)
673        return "MOCK output of child process"
674
675
676class MockOptions(object):
677    """Mock implementation of optparse.Values."""
678
679    def __init__(self, **kwargs):
680        # The caller can set option values using keyword arguments. We don't
681        # set any values by default because we don't know how this
682        # object will be used. Generally speaking unit tests should
683        # subclass this or provider wrapper functions that set a common
684        # set of options.
685        for key, value in kwargs.items():
686            self.__dict__[key] = value
687
688
689class MockPort(Mock):
690    def name(self):
691        return "MockPort"
692
693    def layout_tests_results_path(self):
694        return "/mock/results.html"
695
696    def check_webkit_style_command(self):
697        return ["mock-check-webkit-style"]
698
699    def update_webkit_command(self):
700        return ["mock-update-webkit"]
701
702
703class MockTestPort1(object):
704
705    def skips_layout_test(self, test_name):
706        return test_name in ["media/foo/bar.html", "foo"]
707
708
709class MockTestPort2(object):
710
711    def skips_layout_test(self, test_name):
712        return test_name == "media/foo/bar.html"
713
714
715class MockPortFactory(object):
716
717    def get_all(self, options=None):
718        return {"test_port1": MockTestPort1(), "test_port2": MockTestPort2()}
719
720
721class MockPlatformInfo(object):
722    def display_name(self):
723        return "MockPlatform 1.0"
724
725
726class MockWorkspace(object):
727    def find_unused_filename(self, directory, name, extension, search_limit=10):
728        return "%s/%s.%s" % (directory, name, extension)
729
730    def create_zip(self, zip_path, source_path):
731        pass
732
733
734class MockTool(object):
735
736    def __init__(self, log_executive=False):
737        self.wakeup_event = threading.Event()
738        self.bugs = MockBugzilla()
739        self.buildbot = MockBuildBot()
740        self.executive = MockExecutive(should_log=log_executive)
741        self.filesystem = MockFileSystem()
742        self.workspace = MockWorkspace()
743        self._irc = None
744        self.user = MockUser()
745        self._scm = MockSCM()
746        self._checkout = MockCheckout()
747        self.status_server = MockStatusServer()
748        self.irc_password = "MOCK irc password"
749        self.port_factory = MockPortFactory()
750        self.platform = MockPlatformInfo()
751
752    def scm(self):
753        return self._scm
754
755    def checkout(self):
756        return self._checkout
757
758    def ensure_irc_connected(self, delegate):
759        if not self._irc:
760            self._irc = MockIRC()
761
762    def irc(self):
763        return self._irc
764
765    def path(self):
766        return "echo"
767
768    def port(self):
769        return MockPort()
770
771
772class MockBrowser(object):
773    params = {}
774
775    def open(self, url):
776        pass
777
778    def select_form(self, name):
779        pass
780
781    def __setitem__(self, key, value):
782        self.params[key] = value
783
784    def submit(self):
785        return Mock(file)
786