[PATCH 4 of 5] hgmerge tests

Steve Borho steve at borho.org
Fri Feb 1 04:56:49 UTC 2008


# HG changeset patch
# User Steve Borho <steve at borho.org>
# Date 1201840337 21600
# Node ID 9aca680a4d18a5a8a0f3ea9bff4217e297264f70
# Parent  f3231b697c3d8f31f1c5729c43edbc04b1a52b80
hgmerge tests

diff --git a/tests/test-hgmerge.py b/tests/test-hgmerge.py
new file mode 100755
--- /dev/null
+++ b/tests/test-hgmerge.py
@@ -0,0 +1,894 @@
+#!/usr/bin/env python
+import os.path, tempfile, sys
+import re
+import stat
+from glob import glob
+import unittest
+import shutil
+
+from mercurial import hgmerge
+import mercurial.hgmerge._plugins as mergeplugs
+import mercurial.hgmerge._simplemerge as hgsimplemerge
+import mercurial.hgmerge._pluginapi as mergepluginapi
+import mercurial.util as hgutil
+
+class MthdMock:
+    def __init__(self, returnval=None, func=None):
+        self.mock_args = self.mock_kwds = None
+        self.__retval = returnval
+        self.__args2retval = []
+        self.__func = func
+        self.__exc = None
+
+    def __call__(self, *args, **kwds):
+        self.mock_args = args
+        self.mock_kwds = kwds
+        if self.__func is not None:
+            return self.__func(*args, **kwds)
+        if self.__exc is not None:
+            raise self.__exc
+        for a, r in self.__args2retval:
+            if a == args:
+                retval = r
+                break
+        else:
+            retval = self.__retval
+        return retval
+
+    @property
+    def mock_is_called(self):
+        return self.mock_args is not None
+
+    def mock_set_returnvalue(self, retval):
+        self.__retval = retval
+
+    def mock_set_returnvalue_for_args(self, args, retval):
+        # Note that we can't use a dict since either of the arguments may be
+        # unhashable
+        self.__args2retval.append((args, retval))
+
+    def mock_set_raises(self, exc):
+        self.__exc = exc
+
+class FakeUi(object):
+    def __init__(self, conf=None, globalconf=None):
+        if conf is None: conf = {}
+        if globalconf is None: globalconf = {}
+        self.__conf = conf
+        self.__globalconf = globalconf
+        self.warn = MthdMock()
+        self.info = MthdMock()
+        self.write = MthdMock()
+        self.prompt = MthdMock()
+        self.debug = MthdMock()
+
+    def configitems(self, key):
+        if key == 'merge-tools':
+            return self.__conf.items()
+        elif key == 'merge':
+            return self.__globalconf.items()
+        return {}.items()
+
+    def config(self, sect, field, default=None):
+        if sect == 'merge-tools':
+            return self.__conf.get(field, default)
+        elif sect == 'merge':
+            return self.__globalconf.get(field, default)
+        return default
+
+    def configbool(self, sect, field, default=None):
+        val = self.config(sect, field, default=default)
+        if val is default:
+            return val
+        val = val.lower()
+        if val == 'true':
+            return True
+        if val == 'false':
+            return False
+        raise ValueError(val)
+
+    def configlist(self, sect, field, default=None):
+        val = self.config(sect, field, default=default)
+        if val is default:
+            return val
+        return val.replace(',', ' ').split()
+
+class FakeRepo(object):
+    def __init__(self, conf=None, globalconf=None):
+        self.ui = FakeUi(conf, globalconf)
+        self.root = os.getcwd()
+
+class _testcase(unittest.TestCase):
+    def setUp(self):
+        unittest.TestCase.setUp(self)
+        self.__origattrs = {}
+        self.__tempfiles = []
+        self.__tempdirs = []
+        self.__scriptdir = os.path.abspath(os.path.join("Data", "bin"))
+        self.__outputdir = os.path.join(self.__scriptdir, "Output")
+        if not os.path.exists(self.__outputdir):
+            os.makedirs(self.__outputdir)
+
+    def tearDown(self):
+        unittest.TestCase.tearDown(self)
+        for (obj, attr), val in self.__origattrs.items():
+            setattr(obj, attr, val)
+        for ftemp in self.__tempfiles:
+            if isinstance(ftemp, file):
+                ftemp.close()
+                fname = ftemp.name
+            else:
+                fname = ftemp
+            try: os.remove(fname)
+            except EnvironmentError: pass
+        for dtemp in self.__tempdirs:
+            if not os.path.exists(dtemp):
+                continue
+            shutil.rmtree(dtemp, ignore_errors=True)
+
+    def assertNot(self, val, msg=None):
+        self.assert_(not val, msg=msg)
+
+    def assertIs(self, lhs, rhs, msg=None):
+        if not lhs is rhs:
+            raise self.failureException, msg or "%r is not %r" % (lhs, rhs)
+
+    def assertIsNot(self, lhs, rhs, msg=None):
+        if lhs is rhs:
+            raise self.failureException, msg or "%r is %r" % (lhs, rhs)
+
+    def assertIn(self, val, seq, msg=None):
+        if not val in seq:
+            raise self.failureException, msg or "%r not in %r" % (val, seq)
+
+    def assertNotIn(self, val, seq, msg=None):
+        if val in seq:
+            raise self.failureException, msg or "%r in %r" % (val, seq)
+
+    def _create_repo(self, conf={}):
+        return FakeRepo(conf)
+
+    def _set_objattr(self, obj, attr, val):
+        if (obj, attr) not in self.__origattrs:
+            self.__origattrs[(obj, attr)] = getattr(obj, attr)
+        setattr(obj, attr, val)
+
+    def _get_tempfname(self):
+        """ Get a temporary filename.
+
+        The file is automatically removed upon teardown if still there.
+        """
+        fd, fpath = tempfile.mkstemp()
+        os.close(fd)
+        self.__tempfiles.append(fpath)
+        return fpath
+
+    def _get_tempfile(self, *args, **kwds):
+        fd, fpath = tempfile.mkstemp()
+        os.close(fd)
+        # Open file directly instead of using fdopen, since the latter will
+        # return a file with a bogus name
+        ftemp = file(fpath, "wb+")
+        self.__tempfiles.append(ftemp)
+        return ftemp
+
+    def _get_tempdir(self, *args, **kwds):
+        """ Create temporary directory that is removed on tearDown. """
+        dtemp = tempfile.mkdtemp(*args, **kwds)
+        self.__tempdirs.append(dtemp)
+        return dtemp
+
+class FakeFileCtx(object):
+    def __init__(self, fname, islink):
+        self.fname = fname
+        self.name = fname
+        self._islink = islink
+        self._target = os.readlink(fname) if islink else None
+        self._eoltp = 'unknown'
+
+    def islink(self):
+        return self._islink
+    def path(self):
+        return self.fname
+    def data(self):
+        return self._target
+
+
+class _FakePlugin(object):
+    def __init__(self, name='Fake', result=0, detect=True,
+        priority=0):
+        self.name = name
+        self.priority = priority
+        self._symlink = False
+        self._binary = False
+        self.__result, self.__detect = result, detect
+        self.fake_mergeargs = None
+        self.fake_opts = {}
+        self.fake_mergeexc = None
+
+    @property
+    def fake_is_called(self):
+        return self.fake_mergeargs is not None
+
+    def set_options(self, options, ui):
+        for attr, val in options.attrs.items():
+            self.fake_opts[attr] = val
+        try: self.priority = options.attrs['priority']
+        except KeyError: pass
+
+    def detect(self, ui):
+        return self.__detect
+
+    def merge(self, base, local, other, output, ui):
+        self.fake_mergeargs = (base, local, other, output, ui)
+        if self.fake_mergeexc is not None:
+            raise self.fake_mergeexc
+        return self.__result
+
+def _read_file(fname):
+    f = file(fname, 'rb')
+    try: data = f.read()
+    finally: f.close()
+    return data
+
+def _create_file(fname, content):
+    f = file(fname, 'wb')
+    try: f.write(content)
+    finally: f.close()
+
+class merge_test(_testcase):
+    ''' Test merge function.
+    '''
+    def setUp(self):
+        _testcase.setUp(self)
+        self.__repo = FakeRepo()
+        self.__ui = self.__repo.ui
+        self.__simplemerge = MthdMock(returnval=0)
+        self._set_objattr(hgsimplemerge, 'simplemerge', self.__simplemerge)
+
+    def tearDown(self):
+        _testcase.tearDown(self)
+        assert hgsimplemerge.simplemerge is not self.__simplemerge
+
+    def test_merge(self):
+        ''' Test simple merge, with no conflicts. '''
+        def merge(base, local, other, output, ui, **kwds):
+            self.__mergeargs = (base, local, other, output, ui)
+            f = file(output, 'w')
+            f.write('Success')
+            f.close()
+            return 0
+
+        base, local, other = self.__get_versions()
+        interactive, patmatch = self.__create_plug()
+        self._set_objattr(hgsimplemerge, 'simplemerge', merge)
+
+        r = hgmerge.merge(self.__repo, local, base, other, pluginspec=
+            (interactive, patmatch))
+        self.assertEqual(r, 0)
+        self.__verify_mergeargs(self.__mergeargs, local, base, other)
+        self.assertNot(interactive.fake_is_called)
+        # Now verify merge output
+        f = file(local.name, 'r')
+        txt = f.read()
+        f.close()
+        self.assertEqual(txt, 'Success')
+
+    def test_merge_permissions(self):
+        ''' Verify that the permissions are by default the same as for the local
+        copy.
+        '''
+        base, local, other = self.__get_versions()
+        os.chmod(local.name, 0666)
+        interactive, patmatch = self.__create_plug()
+        hgmerge.merge(self.__repo, local, base, other, pluginspec=
+            (interactive, patmatch))
+        st = os.stat(local.name)
+        self.assertEqual(stat.S_IMODE(st.st_mode), 0666)
+
+    def test_merge_failure(self):
+        ''' Verify behaviour upon merge failure.
+
+        The idea here is to verify that a botched merge operation does not
+        result in permanent damage.
+        '''
+        def merge(local, base, other, output, ui):
+            self.__mergelocal = local
+            f = file(output, 'w')
+            f.write('Botched')
+            f.close()
+            raise Exception()
+
+        base, local, other = self.__get_versions()
+        f = file(local.name, 'w')
+        f.write('My text')
+        f.close()
+        interactive, patmatch = self.__create_plug()
+        self.__simplemerge.mock_set_returnvalue(1)
+        self._set_objattr(interactive, 'merge', merge)
+
+        r = hgmerge.merge(self.__repo, local, base, other, pluginspec=
+            (interactive, patmatch))
+        self.assertEqual(r, 2)
+        # Verify merge output and backup
+        f = file(local.name)
+        txt = f.read()
+        f.close()
+        self.assertEqual(txt, 'Botched')
+        f = file(self.__mergelocal)
+        txt = f.read()
+        f.close()
+        self.assertEqual(txt, 'My text')
+        # Verify that the user is warned
+        self.assert_(self.__ui.warn.mock_args[0].startswith(
+            'merging failed'))
+
+    def test_merge_conflicts(self):
+        ''' Test merge in which conflicts must be resolved. '''
+        base, local, other = self.__get_versions()
+        interactive, patmatch = self.__create_plug()
+        self.__simplemerge.mock_set_returnvalue(1)
+        self.__ui.prompt.mock_set_returnvalue('y')
+
+        r = hgmerge.merge(self.__repo, local, base, other, pluginspec=
+            (interactive, patmatch))
+        self.assertEqual(r, 0)
+        self.__verify_mergeargs(getattr(interactive, 'fake_mergeargs'),
+            local, base, other)
+
+    def test_merge_conflicts_return_none(self):
+        ''' Test having merge plug-in return None. '''
+        base, local, other = self.__get_versions()
+        interactive, patmatch = self.__create_plug(result=None)
+        self.__simplemerge.mock_set_returnvalue(1)
+        self.__ui.prompt.mock_set_returnvalue('y')
+
+        r = hgmerge.merge(self.__repo, local, base, other, pluginspec=
+            (interactive, patmatch))
+        self.assertEqual(r, 0)
+
+    def test_merge_conflicts_no_plugin(self):
+        ''' Test merge where there are conflicts, but no fallback plug-in. '''
+        def merge(base, local, other, output, ui, **kwds):
+            f = file(output, 'w')
+            try: f.write('Conflicts')
+            finally: f.close()
+            return 1
+
+        base, local, other = self.__get_versions()
+        self._set_objattr(hgsimplemerge, 'simplemerge', merge)
+
+        r = hgmerge.merge(self.__repo, local, base, other, pluginspec=
+            (None, False))
+        self.assertEqual(r, 1)
+        self.assertEqual(_read_file(local.name), 'Conflicts')
+        # Make sure no backup is kept
+        self.assertNot(glob('%s.orig.*' % local.name))
+
+    def test_merge_conflicts_dont_resolve(self):
+        ''' Test when user wants to resolve conflicts himself. '''
+        base, local, other = self.__get_versions()
+        interactive, patmatch = self.__create_plug()
+        self.__simplemerge.mock_set_returnvalue(1)
+        self.__ui.prompt.mock_set_returnvalue('y')
+
+        repo = FakeRepo(globalconf={'resolve_conflicts': 'false'})
+        r = hgmerge.merge(repo, local, base, other, pluginspec=
+            (interactive, patmatch))
+        self.assertEqual(r, 1)
+        # The interactive tool should not be invoked
+        self.assertEqual(interactive.fake_mergeargs, None)
+
+    def __verify_mergeargs(self, mergeargs, local, base, other):
+        mlocal, mbase, mother, moutput, mui = mergeargs
+        self.assertEqual(mbase, base.name)
+        self.assert_(re.match(r'^%s\.orig\.\d+' % local.name, mlocal))
+        self.assertEqual(mother, other.name)
+        self.assertEqual(moutput, local.name)
+        self.assertIs(mui, self.__ui)
+
+    def test_merge_plugins_unspecified(self):
+        ''' Test merge without specifying plugins. '''
+        base, local, other = self.__get_versions()
+        interactive, patmatch = self.__create_plug()
+        get_pluginmock = MthdMock(returnval=(interactive, patmatch))
+        self._set_objattr(hgmerge, 'get_plugin', get_pluginmock)
+        repo = self.__repo
+        # Assert that get_plugin is called when no plug-in is specified
+        hgmerge.merge(repo, local, base, other, pluginspec=None)
+        self.assertEqual(get_pluginmock.mock_args, (repo, local.name))
+
+    def test_merge_plugin_none(self):
+        ''' Test having get_plugin return no plug-in while one was requested
+        for this filetype.
+        '''
+        base, local, other = self.__get_versions()
+        get_pluginmock = MthdMock(returnval=(None, True))
+        self._set_objattr(hgmerge, 'get_plugin', get_pluginmock)
+        self.assertEqual(hgmerge.merge(self.__repo, local, base, other),
+            2)
+
+    def test_merge_unmergeable_success(self):
+        ''' Test merging where file doesn't appear mergeable, but user chooses
+        one version.
+        '''
+        base, local, other = self.__get_versions()
+        interactive, patmatch = self.__create_plug()
+        ask_unmergeable = MthdMock(returnval=True)
+        self._set_objattr(hgmerge, '_ask_unmergeable', ask_unmergeable)
+        self._set_objattr(hgmerge, '_is_mergeable', MthdMock(returnval=False))
+
+        r = hgmerge.merge(self.__repo, local, base, other, pluginspec=
+            (interactive, patmatch))
+        self.assertEqual(r, 0)
+        self.assert_(ask_unmergeable.mock_is_called)
+
+    def test_merge_unmergeable_failure(self):
+        ''' Test merging where file doesn't appear mergeable, and user doesn't
+        choose one version.
+        '''
+        base, local, other = self.__get_versions()
+        interactive, patmatch = self.__create_plug()
+        ask_unmergeable = MthdMock(returnval=False)
+        self._set_objattr(hgmerge, '_ask_unmergeable', ask_unmergeable)
+        self._set_objattr(hgmerge, '_is_mergeable', MthdMock(returnval=False))
+
+        r = hgmerge.merge(self.__repo, local, base, other, pluginspec=
+            (interactive, patmatch))
+        self.assertEqual(r, 2)
+
+    def test__is_mergeable(self):
+        base, local, other = self.__get_versions()
+        interactive, patmatch = self.__create_plug()
+        self._set_objattr(hgmerge, "_eoltype", MthdMock(returnval='unix'))
+        self.assert_(hgmerge._is_mergeable(interactive, patmatch, base,
+            local, other))
+
+    def test__is_mergeable_false(self):
+        base, local, other = self.__get_versions()
+        interactive, patmatch = self.__create_plug()
+        eoltype = MthdMock()
+        self._set_objattr(hgmerge, '_eoltype', eoltype)
+        for mergeable, unmergeable in ((local, other), (other, local)):
+            eoltype.mock_set_returnvalue_for_args((mergeable,), 'unix')
+            eoltype.mock_set_returnvalue_for_args((unmergeable,), 'binary')
+            self.assertNot(hgmerge._is_mergeable(interactive,
+                patmatch, base, local, other))
+
+    def test__ask_unmergeable_local(self):
+        ''' Test _ask_unmergeable_method, where user picks local version. '''
+        base, local, other = self.__get_versions()
+        ui = self.__ui
+        ui.prompt.mock_set_returnvalue('k')
+        r = hgmerge._ask_unmergeable(self.__ui, local, other, local)
+        self.assert_(r)
+
+    def test__ask_unmergeable_other(self):
+        ''' Test _ask_unmergeable_method, where user picks other version. '''
+        base, local, other = self.__get_versions()
+        ui = self.__ui
+        ui.prompt.mock_set_returnvalue('o')
+        r = hgmerge._ask_unmergeable(self.__ui, local, other, local)
+        self.assert_(r)
+
+    def test__ask_unmergeable_fail(self):
+        ''' Test _ask_unmergeable_method, where user fails to pick a version.
+        '''
+        # TODO: Find out how user cancellation is communicated
+#        raise NotImplementedError()
+
+    def test__ask_unmergeable_binary(self):
+        ''' Test _ask_unmergeable_method, where one version is binary.
+
+        For coverage.
+        '''
+        base, local, other = self.__get_versions()
+        ui = self.__ui
+        ui.prompt.mock_set_returnvalue('o')
+        self._set_objattr(local, '_eoltp', 'binary')
+        hgmerge._ask_unmergeable(self.__ui, local, other, local)
+
+    def test__ask_unmergeable_other_symlink(self):
+        ''' Test _ask_unmergeable_method, where the other version is a symlink.
+        '''
+        ui = self.__ui
+        ui.prompt.mock_set_returnvalue('o')
+        local, linkdest, other = (self._get_tempfname(), self._get_tempfname(),
+                self._get_tempfname())
+        os.remove(other)
+        os.symlink(linkdest, other)
+
+        local = FakeFileCtx(local, False)
+        other = FakeFileCtx(other, True)
+        r = hgmerge._ask_unmergeable(self.__ui, local, other, local)
+        self.assert_(r)
+        self.assertEqual(os.readlink(local.name), linkdest)
+
+    def test__ask_unmergeable_unknown_eol(self):
+        ''' Test _ask_unmergeable_method, where one version has an unknown EOL
+        type.
+
+        For coverage.
+        '''
+        base, local, other = self.__get_versions()
+        ui = self.__ui
+        ui.prompt.mock_set_returnvalue('o')
+        self._set_objattr(local, '_eoltp', 'unknown')
+        hgmerge._ask_unmergeable(self.__ui, local, other, local)
+
+    def __create_plug(self, result=0):
+        """ Create mock plug.
+        """
+        plug = _FakePlugin(result=result)
+        return plug, False
+
+    def __get_versions(self, basecontent=None, localcontent=None,
+            othercontent=None, base_islink=False, local_islink=False,
+            other_islink=False):
+        """ Create three different file versions for merging. """
+        if basecontent is None:
+            basecontent = "First line\n"
+        if localcontent is None:
+            localcontent = "%sMy change\n" % (basecontent,)
+        if othercontent is None:
+            othercontent = "My change\n%s" % (basecontent,)
+
+        base = self._get_tempfname()
+        f = file(base, "w")
+        try: f.write(basecontent)
+        finally: f.close()
+        local, other = self._get_tempfname(), self._get_tempfname()
+        f = file(local, "w")
+        try: f.write(localcontent)
+        finally: f.close()
+        f = file(other, "w")
+        try: f.write(othercontent)
+        finally: f.close()
+
+        return (FakeFileCtx(base, base_islink), FakeFileCtx(local,
+            local_islink), FakeFileCtx(other, other_islink))
+
+class get_plugin_test(_testcase):
+    ''' Test get_plugin function.
+    '''
+    def setUp(self):
+        _testcase.setUp(self)
+
+    def test_get_plugin(self):
+        ''' Test the get_plugin method. '''
+        self._set_objattr(hgmerge, 'plugins', [_FakePlugin()])
+        repo = FakeRepo()
+        interactive, patmatch = self.__get_plugin(repo=repo)
+        self.assertIs(interactive, hgmerge.plugins[0])
+        self.assertNot(patmatch)
+        # There should be no warnings to the user
+        self.assertNot(repo.ui.warn.mock_is_called)
+
+    def test_get_plugin_none(self):
+        ''' Test get_plugin when no interactive plug-in is available. '''
+        self._set_objattr(hgmerge, 'plugins', [_FakePlugin(detect=
+            False)])
+        self.assertEqual(self.__get_plugin(), (None, False))
+
+    def test_get_plugin_initial(self):
+        ''' Ensure that get_plugin calls _setup_plugs initially. '''
+        def setup_plugs(ui):
+            self._set_objattr(hgmerge, 'plugins', [])
+
+        # Make sure this is not defined
+        self._set_objattr(hgmerge, 'plugins', None)
+        setupmock = MthdMock(func=setup_plugs)
+        self._set_objattr(hgmerge, '_setup_plugs', setupmock)
+        repo = FakeRepo()
+        self.assertIs(self.__get_plugin(repo)[0], None)
+        self.assertEqual(setupmock.mock_args, (repo.ui,))
+
+    def test_get_plugin_hgrc_hgmerge_plugin(self):
+        ''' Make sure hgrc parameter 'default' is respected. '''
+        self._set_objattr(hgmerge, 'plugins', [_FakePlugin(),
+            _FakePlugin(name='test2')])
+        repo = FakeRepo(globalconf={'default': 'test2'})
+        plug, patmatch = self.__get_plugin(repo)
+        self.assertEqual(plug.name, 'test2')
+        self.assertNot(patmatch)
+
+    def test_get_plugin_hgrc_ext(self):
+        ''' Make sure hgrc file pattern matchin is respected. '''
+        self._set_objattr(hgmerge, 'plugins', [_FakePlugin(),
+            _FakePlugin(name='test2')])
+        inter, patmatch = self.__get_plugin(FakeRepo(globalconf=
+            {'*.png': 'test2'}), fname='test.png')
+        self.assertEqual(inter.name, 'test2')
+        self.assert_(patmatch)
+
+    def test_get_plugin_hgrc_ext_missing(self):
+        ''' Test when specified plug-in for a file pattern is missing. '''
+        self._set_objattr(hgmerge, 'plugins', [_FakePlugin(),
+            _FakePlugin(name='test2', detect=False)])
+        ret = self.__get_plugin(FakeRepo(globalconf=
+            {'*.png': 'test2'}), fname='test.png')
+        self.assertEqual(ret, (None, True))
+
+    def test_get_plugin_special(self):
+        ''' Test getting special plug-ins. '''
+        for name in ('takelocal', 'takeother'):
+            repo = FakeRepo(globalconf={'default': name})
+            plug, patmatch = self.__get_plugin(repo)
+            self.assertEqual(plug.name, name)
+
+    def __get_plugin(self, repo=None, fname=None):
+        if repo is None:
+            repo = self._create_repo()
+        return hgmerge.get_plugin(repo, fname=fname)
+
+    def test__setup_plugs_modify(self):
+        ''' Test modifying plug via _setup_plugs. '''
+        args = '$local $base $other'
+        self.__setup_plugs({'test.args': args})
+        self.assertEqual(self.__fakeplug.fake_opts, {'args': args.split(),})
+
+    def test__setup_plugs_define(self):
+        ''' Test defining tool via _setup_plugs. '''
+        self.__setup_plugs({'newplug': ''})
+        # Note that user-defined plug-ins should have priority 1
+        plug = hgmerge.plugins[-1]
+        self.assertEqual(plug.name, 'newplug')
+
+    def test__setup_plugs_define_invalid(self):
+        ''' Test providing invalid definition for tool. '''
+        def check_plugs():
+            ''' Verify that the plug-ins aren't changed. '''
+            self.assertEqual(hgmerge.plugins,
+                mergeplugs.plugins)
+
+        conf = {'newplug.priority': 'invalid'}
+        self.__setup_plugs(conf)
+        check_plugs()
+        conf = {'newplug.stdout': 'invalid'}
+        self.__setup_plugs(conf)
+        check_plugs()
+
+    def test__setup_plugs_priority(self):
+        ''' Test defining plug-in with priority. '''
+        self.__setup_plugs({'newplug.priority': '1'})
+        self.assertEqual(hgmerge.plugins[0].name, 'newplug')
+
+    def test__setup_plugs_priority_modify(self):
+        ''' Test modifying plug-in's priority. '''
+        self.__setup_plugs({'test.priority': '10'})
+        self.assertEqual(hgmerge.plugins[0].priority, 10)
+
+    def test_query_plugins(self):
+        def setup_plugs(ui):
+            self._set_objattr(hgmerge, 'plugins', [
+                _FakePlugin(name='test1', detect=False), _FakePlugin(
+                    name='test2')])
+        self._set_objattr(hgmerge, 'plugins', None)
+        self._set_objattr(hgmerge, '_setup_plugs', setup_plugs)
+        self.assertEqual(hgmerge.query_plugins(FakeUi())[0].name, 'test2')
+
+    def test_query_plugins_rescan(self):
+        setupmock = MthdMock()
+        self._set_objattr(hgmerge, 'plugins', [])
+        self._set_objattr(hgmerge, '_setup_plugs', setupmock)
+        hgmerge.query_plugins(FakeUi(), rescan=True)
+        self.assert_(setupmock.mock_is_called)
+
+    def __setup_plugs(self, conf):
+        plug = self.__fakeplug = _FakePlugin('test')
+        self._set_objattr(mergeplugs, 'plugins', [plug])
+        ui = self.__ui = FakeUi(conf)
+        hgmerge._setup_plugs(ui)
+
+class hgmerge_misc_test(_testcase):
+    ''' Test miscellaneous in the hgmerge package.
+    '''
+    def test_stocktools(self):
+        ''' Test the stocktools variable.
+        '''
+        self.assertIs(hgmerge.stocktools, mergeplugs.plugins)
+        self.assert_(isinstance(hgmerge.stocktools, (list, tuple)))
+
+class simplemerge_test(_testcase):
+    ''' Test _simplemerge module. '''
+    def test_simplemerge(self):
+        base = self._create_file('Base\n\n')
+        local = self._create_file('Base\n\nLocal\n')
+        other = self._create_file('Other\n\n')
+        output = self._get_tempfname()
+        r = hgsimplemerge.simplemerge(local, base, other, output, FakeUi())
+        self.assertEqual(r, 0)
+        self.assertEqual(_read_file(output), 'Other\n\nLocal\n')
+
+    def test_simplemerge_conflicts(self):
+        ''' Test simplemerge on conflicting changes. '''
+        base = self._create_file('\n')
+        local = self._create_file('Local\n')
+        other = self._create_file('Other\n')
+        output = self._get_tempfname()
+        r = hgsimplemerge.simplemerge(local, base, other, output, FakeUi())
+        self.assertEqual(r, 1)
+
+        self.assertEqual(_read_file(output), '''<<<<<<< %s
+Local
+=======
+Other
+>>>>>>> %s
+''' % (local, other))
+
+    def _create_file(self, contents='', fname=None):
+        if fname is None:
+            fname = self._get_tempfname()
+        f = file(fname, 'w')
+        try: f.write(contents)
+        finally: f.close()
+        return fname
+
+class toolplugin_test(_testcase):
+    ''' Test toolplugin class.
+    '''
+    def setUp(self):
+        _testcase.setUp(self)
+        self.__ui = FakeUi()
+
+    def test_construct_exe_none(self):
+        ''' Test constructing with None for executable.
+        '''
+        plug = self.__create_plug()
+        self.assertEqual(plug.executable, plug.name.lower())
+
+    def test__detect_in_path(self):
+        plug = self.__create_plug()
+        self._set_objattr(hgutil, 'find_exe', MthdMock(returnval='/test'))
+        self.assertEqual(plug._detect_in_path(), '/test')
+
+    def test__detect_in_reg(self):
+        ''' Test _detect_in_reg. '''
+        try: import mercurial.util_win32 as hgutil_win32
+        except ImportError:
+            # Not available anyway
+            return
+        plug = self.__create_plug(winreg_key='SOFTWARE', winreg_name='test')
+        lookupmock = MthdMock(returnval='/test')
+        self._set_objattr(hgutil_win32, 'lookup_reg', lookupmock)
+        self._set_objattr(os, 'access', MthdMock(returnval=True))
+        self.assertEqual(plug._detect_in_reg(), '/test')
+        self.assertEqual(lookupmock.mock_args, ('SOFTWARE', 'test'))
+
+    def test__detect_in_reg_append(self):
+        ''' Test _detect_in_reg when winreg_append is set. '''
+        try: import mercurial.util_win32 as hgutil_win32
+        except ImportError:
+            # Not available anyway
+            return
+        plug = self.__create_plug(winreg_key='SOFTWARE', winreg_name='test',
+                winreg_append='/test.exe')
+        lookupmock = MthdMock(returnval='/test')
+        self._set_objattr(hgutil_win32, 'lookup_reg', lookupmock)
+        self._set_objattr(os, 'access', MthdMock(returnval=True))
+        self.assertEqual(plug._detect_in_reg(), '/test/test.exe')
+        self.assertEqual(lookupmock.mock_args, ('SOFTWARE', 'test'))
+
+    def test__detect_in_reg_not_available(self):
+        ''' Test _detect_in_reg when the method is not available to us. '''
+        plug = self.__create_plug(winreg_key='SOFTWARE', winreg_name='test',
+                winreg_append='/test.exe')
+        try: import mercurial.util_win32 as hgutil_win32
+        except ImportError:
+            pass
+        else:
+            lookupmock = MthdMock(returnval='/test')
+            lookupmock.mock_set_raises(ImportError)
+            self._set_objattr(hgutil_win32, 'lookup_reg', lookupmock)
+
+        self.assertIs(plug._detect_in_reg(), None)
+
+    def test_detect(self):
+        plug = self.__create_plug()
+        self.__detect(plug, path='/test')
+
+    def test_detect_false(self):
+        plug = self.__create_plug()
+        self.__detect(plug)
+
+    def __detect(self, plug, path=None, reg=None):
+        pathmock = plug._detect_in_path = MthdMock(returnval=path)
+        regmock = plug._detect_in_reg = MthdMock(returnval=reg)
+        r = plug.detect(self.__ui)
+        self.assert_(pathmock.mock_is_called)
+        if path is None:
+            self.assert_(regmock.mock_is_called)
+        if path is not None or reg is not None:
+            self.assert_(r)
+        else:
+            self.assertNot(r)
+
+    def test_merge(self):
+        plug = self.__create_plug()
+        plug.executable = 'test'
+        self.assertEqual(self.__merge(plug), 0)
+        # The standard tool arguments are $output $base $other
+        base, other, output = self.__mergeargs[1:4]
+        self.assertEqual(self.__runmock.mock_args, ([plug.executable] +
+            [output, base, other], self.__ui))
+
+    def test_merge_stdout(self):
+        ''' Test merging to stdout.
+        '''
+        plug = self.__create_plug(stdout=True)
+        self.__merge(plug)
+        ofile = self.__runmock.mock_kwds['stdout']
+        self.assertEqual(ofile.name, self.__mergeargs[-1])
+        try: os.remove(ofile.name)
+        except EnvironmentError: pass
+
+    def test_merge_check_conflicts(self):
+        ''' Test checking for conflicts after merge.
+        '''
+        plug = self.__create_plug(check_conflicts=True)
+        self.__merge(plug)
+        self.assertEqual(self.__check_conflictsmock.mock_args[0],
+            self.__mergeargs[-1])
+
+    def test__lookup_reg(self):
+        try:
+            import mercurial.util_win32 as hgutil_win32
+            lookupmock = MthdMock(returnval="testval")
+            self._set_objattr(hgutil_win32, "lookup_reg", lookupmock)
+        except ImportError:
+            return
+        self.assertEqual(mergepluginapi._lookup_reg("\\Software\\Test"), testval)
+
+    def __merge(self, plug, retval=0, conflict=False):
+        self.__runmock = MthdMock(returnval=retval)
+        self._set_objattr(mergepluginapi, 'runcommand', self.__runmock)
+        self.__check_conflictsmock = MthdMock(returnval=conflict)
+        self._set_objattr(mergepluginapi, 'checkconflicts',
+            self.__check_conflictsmock)
+        args = self.__mergeargs = ['local', 'base', 'other', 'output']
+        return plug.merge(*(args + [self.__ui]))
+
+    def __create_plug(self, executable=None, args=None, **kwds):
+        plug = mergeplugs.toolplugin('Test', executable, args, **kwds)
+        return plug
+
+class _FileMock(list):
+    def close(self):
+        pass
+
+class misc_test(_testcase):
+    ''' Test miscellaneous.
+    '''
+    def test__checkconflicts(self):
+        txt = '''Testing
+<<<<<<<
+=======
+>>>>>>>
+'''
+        self.__check_conflicts(txt, True)
+
+    def test__checkconflicts_false(self):
+        ''' Test _checkconflicts with no conflicts. '''
+        txt = '''No
+conflicts
+here
+'''
+        self.__check_conflicts(txt, False)
+
+    def __check_conflicts(self, txt, conflicts):
+        filemock = _FileMock(txt.splitlines())
+        filemock.close = MthdMock()
+        import __builtin__
+        self._set_objattr(__builtin__, 'open', MthdMock(returnval=filemock))
+        self.assertEqual(mergepluginapi.checkconflicts('test'), conflicts)
+
+
+if __name__ == "__main__":
+    # hide the timer
+    import time
+    orig = time.time
+    try:
+        time.time = lambda: 0
+        unittest.main()
+    finally:
+        time.time = orig
diff --git a/tests/test-hgmerge.py.out b/tests/test-hgmerge.py.out
new file mode 100644
--- /dev/null
+++ b/tests/test-hgmerge.py.out
@@ -0,0 +1,5 @@
+.................................................
+----------------------------------------------------------------------
+Ran 49 tests in 0.000s
+
+OK
diff --git a/tests/test-simplemerge.py b/tests/test-simplemerge.py
--- a/tests/test-simplemerge.py
+++ b/tests/test-simplemerge.py
@@ -20,12 +20,8 @@
 import imp
 import shutil
 from mercurial import util
+from mercurial.hgmerge import _simplemerge as simplemerge
 
-# copy simplemerge to the cwd to avoid creating a .pyc file in the source tree
-shutil.copyfile(os.path.join(os.environ['TESTDIR'], os.path.pardir,
-                             'contrib', 'simplemerge'),
-                'simplemerge.py')
-simplemerge = imp.load_source('simplemerge', 'simplemerge.py')
 Merge3 = simplemerge.Merge3
 CantReprocessAndShowBase = simplemerge.CantReprocessAndShowBase
 


More information about the Mercurial-devel mailing list