예제 #1
0
def makegitrepo(tmpdir, name):
    path = os.path.join(tmpdir, name)

    def system(cmd):
        check_call(cmd, cwd=path)

    os.mkdir(path)
    system(GIT + ['init'])
    readme = os.path.join(path, 'README.md')
    contents(readme, "Hello\n")
    system(GIT + ['add', 'README.md'])
    system(GIT + ['commit', '-m', 'Added readme'])
    return path
예제 #2
0
 def _addfake(name, createfile):
     # create a fake repo and add it
     tr = TempRepo(tmpdir, name)
     tf = os.path.join(HOME, createfile)
     contents(
         tr.remotepath + '/HOMELY.py', """
              from homely.files import lineinfile
              lineinfile('~/%s', 'Hello from %s')
              """ % (createfile, name))
     assert not os.path.exists(tf)
     system(HOMELY('add') + [tr.url])
     assert contents(tf) == "Hello from %s\n" % name
     return tr
예제 #3
0
def test_homely_remove(tmpdir, HOME):
    system = getsystemfn(HOME)

    def _addfake(name, createfile):
        # create a fake repo and add it
        tr = TempRepo(tmpdir, name)
        tf = os.path.join(HOME, createfile)
        contents(
            tr.remotepath + '/HOMELY.py', """
                 from homely.files import lineinfile
                 lineinfile('~/%s', 'Hello from %s')
                 """ % (createfile, name))
        assert not os.path.exists(tf)
        system(HOMELY('add') + [tr.url])
        assert contents(tf) == "Hello from %s\n" % name
        return tr

    r1 = _addfake('repo1', 'file1.txt')
    r2 = _addfake('repo2', 'file2.txt')
    r3 = _addfake('repo3', 'file3.txt')

    # check that all the repos are there
    checkrepolist(HOME, system, [r1, r2, r3])
    assert contents(HOME + '/file1.txt', "Hello from repo1\n")
    assert contents(HOME + '/file2.txt', "Hello from repo2\n")
    assert contents(HOME + '/file3.txt', "Hello from repo3\n")

    # Check that the repo can be removed.
    system(HOMELY('forget') + [r1.repoid])
    checkrepolist(HOME, system, [r2, r3])
    assert contents(HOME + '/file1.txt', "Hello from repo1\n")
    assert contents(HOME + '/file2.txt', "Hello from repo2\n")
    assert contents(HOME + '/file3.txt', "Hello from repo3\n")

    # now run an update to make repo1's files go away
    system(HOMELY('update'))
    assert not os.path.exists(HOME + '/file1.txt')
    assert contents(HOME + '/file2.txt', "Hello from repo2\n")
    assert contents(HOME + '/file3.txt', "Hello from repo3\n")

    # Test removing multiple repos, but using local path this time
    # Note that because we don't use --update, the created files will still be
    # sitting around on disk
    system(HOMELY('forget') + ['~/repo2', '~/repo3'])
    checkrepolist(HOME, system, [])
    # repo2 and repo3 are stilling going to hang around on disk
    assert os.path.exists(HOME + '/repo2')
    assert os.path.exists(HOME + '/repo3')
    assert not os.path.exists(HOME + '/file1.txt')
    assert contents(HOME + '/file2.txt', "Hello from repo2\n")
    assert contents(HOME + '/file3.txt', "Hello from repo3\n")
예제 #4
0
 def _addfake(name, createfile):
     # create a fake repo and add it
     tr = TempRepo(tmpdir, name)
     tf = os.path.join(tr.remotepath, createfile)
     with open(tf, 'w') as f:
         f.write('hello world')
     contents(
         tr.remotepath + '/HOMELY.py', """
              from homely.files import symlink
              symlink('%s', '~/%s')
              """ % (createfile, createfile))
     system(HOMELY('add') + [tr.url])
     local = '%s/%s' % (tr.suggestedlocal(HOME), createfile)
     assert os.readlink('%s/%s' % (HOME, createfile)) == local
예제 #5
0
def test_cleanup_everything(tmpdir):
    '''
    test that recreating the engine, running nothing on it, then calling
    cleanup() will remove all of things that might be lying around
    '''
    from homely._engine2 import Engine
    from homely.files import MakeDir, MakeSymlink, LineInFile

    cfgpath = gettmpfilepath(tmpdir, '.json')
    d1 = gettmpfilepath(tmpdir, '.d')
    d1f1 = os.path.join(d1, 'sub-file.txt')
    l1 = gettmpfilepath(tmpdir, '.lnk')

    e = Engine(cfgpath)
    e.run(MakeDir(d1))
    e.run(MakeSymlink(d1, l1))
    e.run(LineInFile(d1f1, "AAA"))
    e.cleanup(e.RAISE)
    del e

    assert os.path.isdir(d1)
    assert contents(d1f1, "AAA\n")
    assert os.path.islink(l1) and os.readlink(l1) == d1

    # if we recreate the engine with nothing on it, it should clean everything
    # up
    e = Engine(cfgpath)
    e.cleanup(e.RAISE)

    assert not os.path.exists(d1)
    assert not os.path.islink(l1)
예제 #6
0
def test_lineinfile_knows_about_ownership(HOME, tmpdir):
    system = getsystemfn(HOME)

    # put the 'AAA' line into my file1.txt
    f1 = HOME + '/file1.txt'
    contents(f1, 'AAA\n')

    # create a fake repo and add it
    tr = TempRepo(tmpdir, 'dotfiles')
    contents(tr.remotepath + '/HOMELY.py',
             """
             from homely.general import lineinfile
             lineinfile('file1.txt', 'AAA')
             lineinfile('file1.txt', 'BBB')
             """)
    system(HOMELY('add') + [tr.url])

    # check that my file1.txt now has both lines
    assert contents(f1) == "AAA\nBBB\n"

    # now remove the repo and do a full update to trigger cleanup
    system(HOMELY('forget') + [tr.url])
    system(HOMELY('update'))

    # run homely update a few more times to confuse it
    system(HOMELY('update'))
    system(HOMELY('update'))

    # check that only the 'BBB' line was removed
    assert contents(f1) == "AAA\n"
예제 #7
0
def test_homely_update(HOME, tmpdir):
    system = getsystemfn(HOME)

    def _addfake(name, createfile):
        # create a fake repo and add it
        tr = TempRepo(tmpdir, name)
        tf = os.path.join(HOME, createfile)
        contents(
            tr.remotepath + '/HOMELY.py', """
                 from homely.files import lineinfile
                 lineinfile('~/%s', 'Hello from %s')
                 """ % (createfile, name))
        assert not os.path.exists(tf)
        system(HOMELY('add') + [tr.url])
        assert contents(tf) == "Hello from %s\n" % name
        return tr

    template = ("""
                from homely.files import lineinfile
                lineinfile(%r, %r)
                """)

    # add some fake repos
    r1 = _addfake('repo1', 'file1.txt')
    r2 = _addfake('repo2', 'file2.txt')
    r3 = _addfake('repo3', 'file3.txt')

    # update the repos slightly, run an update, ensure all changes have gone
    # through (including cleanup of old content)
    contents(r1.remotepath + '/HOMELY.py', template % ('~/file1.txt', 'AAA'))
    contents(r2.remotepath + '/HOMELY.py', template % ('~/file2.txt', 'AAA'))
    contents(r3.remotepath + '/HOMELY.py', template % ('~/file3.txt', 'AAA'))
    system(HOMELY('update'))
    assert contents(HOME + '/file1.txt') == "AAA\n"
    assert contents(HOME + '/file2.txt') == "AAA\n"
    assert contents(HOME + '/file3.txt') == "AAA\n"

    # modify all the repos again
    contents(r1.remotepath + '/HOMELY.py', template % ('~/file1.txt', 'BBB'))
    contents(r2.remotepath + '/HOMELY.py', template % ('~/file2.txt', 'BBB'))
    contents(r3.remotepath + '/HOMELY.py', template % ('~/file3.txt', 'BBB'))
    # run an update again, but only do it with the 2nd repo
    system(HOMELY('update') + ['~/repo2'])
    # 1st and 3rd repos haven't been rerun
    assert contents(HOME + '/file1.txt') == "AAA\n"
    assert contents(HOME + '/file3.txt') == "AAA\n"
    # NOTE that the cleanup of the 2nd repo doesn't happen when we're doing a
    # single repo
    assert contents(HOME + '/file2.txt') == "AAA\nBBB\n"

    # run an update, but specify all repos - this should be enough to trigger
    # the cleanup
    system(HOMELY('update') + ['~/repo1', '~/repo3', '~/repo2'])
    assert contents(HOME + '/file1.txt') == "BBB\n"
    assert contents(HOME + '/file2.txt') == "BBB\n"
    assert contents(HOME + '/file3.txt') == "BBB\n"

    # modify the repos again, but run update with --nopull so that we don't get
    # any changes
    contents(r1.remotepath + '/HOMELY.py', template % ('~/file1.txt', 'CCC'))
    contents(r2.remotepath + '/HOMELY.py', template % ('~/file2.txt', 'CCC'))
    contents(r3.remotepath + '/HOMELY.py', template % ('~/file3.txt', 'CCC'))
    system(HOMELY('update') + ['~/repo1', '--nopull'])
    assert contents(HOME + '/file1.txt') == "BBB\n"
    assert contents(HOME + '/file2.txt') == "BBB\n"
    assert contents(HOME + '/file3.txt') == "BBB\n"
    system(HOMELY('update') + ['--nopull'])
    assert contents(HOME + '/file1.txt') == "BBB\n"
    assert contents(HOME + '/file2.txt') == "BBB\n"
    assert contents(HOME + '/file3.txt') == "BBB\n"

    # split r1 into multiple sections and just do one of them
    contents(
        r1.remotepath + '/HOMELY.py', """
             from homely.files import lineinfile
             from homely.general import section
             @section
             def partE():
                lineinfile('~/file1.txt', 'EEE')
             @section
             def partF():
                lineinfile('~/file1.txt', 'FFF')
             @section
             def partG():
                lineinfile('~/file1.txt', 'GGG')
             lineinfile('~/file1.txt', 'HHH')
             """)
    system(HOMELY('update') + ['~/repo1', '-o', 'partE'])
    assert contents(HOME + '/file1.txt') == "BBB\nEEE\nHHH\n"
    os.unlink(HOME + '/file1.txt')
    system(HOMELY('update') + ['~/repo1', '-o', 'partF', '-o', 'partG'])
    assert contents(HOME + '/file1.txt') == "FFF\nGGG\nHHH\n"
    system(HOMELY('update') + ['~/repo1', '-o', 'partF', '-o', 'partG'])
    assert contents(HOME + '/file1.txt') == "FFF\nGGG\nHHH\n"

    # ensure that --only isn't allowed with multiple repos
    system(HOMELY('update') + ['~/repo1', '~/repo2', '-o', 'something'],
           expecterror=1)

    # test that cleanup stuff doesn't happen when --only is used
    system(HOMELY('forget') + ['~/repo2', '~/repo3'])
    assert os.path.exists(HOME + '/file2.txt')
    assert os.path.exists(HOME + '/file3.txt')
    # note that this test also proves that you can use --only without naming a
    # repo as long as you only have one repo
    system(HOMELY('update') + ['-o', 'partE'])
    assert contents(HOME + '/file1.txt') == "FFF\nGGG\nHHH\nEEE\n"
    # these files are still hanging around because we keep using --only
    assert os.path.exists(HOME + '/file2.txt')
    assert os.path.exists(HOME + '/file3.txt')

    # finally do an update without --only, and file1 and file2 will be cleaned
    # up
    system(HOMELY('update'))
    assert not os.path.exists(HOME + '/file2.txt')
    assert not os.path.exists(HOME + '/file3.txt')
예제 #8
0
def test_writefile_usage(tmpdir):
    from homely._engine2 import Engine
    from homely.files import LineInFile
    from homely.general import WriteFile, writefile

    cfgpath = gettmpfilepath(tmpdir, '.json')

    # the file we'll be playing with
    f1 = os.path.join(tmpdir, 'f1.txt')
    f2 = os.path.join(tmpdir, 'f2.txt')
    f3 = os.path.join(tmpdir, 'f3.json')

    # use LineInFile to put stuff in the file
    e = Engine(cfgpath)
    e.run(LineInFile(f1, 'AAA'))
    e.run(LineInFile(f1, 'BBB'))
    del e
    assert contents(f1) == "AAA\nBBB\n"

    # now use WriteFile() to give it new contents
    e = Engine(cfgpath)
    e.run(WriteFile(f1, "BBB\nCCC\n"))
    assert contents(f1) == "BBB\nCCC\n"
    e.cleanup(e.RAISE)
    del e
    # make sure the cleanup didn't blow anything away
    assert contents(f1) == "BBB\nCCC\n"

    # make sure the file is removed on cleanup
    e = Engine(cfgpath)
    e.cleanup(e.RAISE)
    del e
    assert not os.path.exists(f1)

    contents(f2, "Already here!\n")
    assert os.path.exists(f2)
    e = Engine(cfgpath)
    e.run(WriteFile(f2, "AAA\nBBB\n", canoverwrite=True))
    e.cleanup(e.RAISE)
    del e
    assert contents(f2) == "AAA\nBBB\n"

    # running a LineInFile() won't clean up what's already there
    e = Engine(cfgpath)
    e.run(LineInFile(f2, "CCC"))
    e.cleanup(e.RAISE)
    del e
    assert contents(f2) == "AAA\nBBB\nCCC\n"

    # note that removing the LineInFile() doesn't clean up the file because we
    # no longer know whether the user put their config in there
    e = Engine(cfgpath)
    e.cleanup(e.RAISE)
    del e
    assert not os.path.exists(f1)
    assert os.path.exists(f2)

    # now test the context manager
    import homely._engine2
    e = Engine(cfgpath)
    homely._engine2._ENGINE = e
    data = {"z": [3, 4, 5, True], "y": "Hello world", "x": None}
    with writefile(f3) as f:
        if sys.version_info[0] < 3:
            f.write(simplejson.dumps(data, ensure_ascii=False))
        else:
            f.write(simplejson.dumps(data))
    e.cleanup(e.RAISE)
    del e
    assert os.path.exists(f3)
    with open(f3, 'r') as f:
        assert simplejson.loads(f.read()) == data

    # prove that the WriteFile() disappearing results in the file being removed
    e = Engine(cfgpath)
    e.cleanup(e.RAISE)
    del e
    assert not os.path.exists(f3)
예제 #9
0
def test_blockinfile_lineinfile_cleanup_interaction(tmpdir):
    '''
    Test that when a LineInFile is cleaned up, any other LineInFile() or
    BlockInFile() that affected the same file, needs a chance to re-check
    whether it is still valid. If *any* of the remaining Helpers needs to be
    reapplied, then they *all* need to be reapplied.
    '''
    from homely._engine2 import Engine
    from homely.files import LineInFile, BlockInFile

    cfgpath = gettmpfilepath(tmpdir, '.json')
    f1 = gettmpfilepath(tmpdir, '.txt')

    def bif(filename, lines):
        # shorthand constructor for BlockInFile()
        return BlockInFile(filename, lines, None, "PRE", "POST")

    # check that a LineInFile followed by a BlockInFile that both try to add
    # the same line will result in the file containing both things, even if
    # you rerun it many times
    for i in range(3):
        e = Engine(cfgpath)
        e.run(LineInFile(f1, "AAA"))
        e.run(bif(f1, ["AAA"]))
        e.cleanup(e.RAISE)
        del e
        assert contents(f1) == "AAA\nPRE\nAAA\nPOST\n"

    # check that a BlockInFile followed by a LineInFile that adds the same line
    # will result in the LineInFile not having any effect, even if you rerun it
    # many times
    os.unlink(f1)
    for i in range(3):
        e = Engine(cfgpath)
        e.run(bif(f1, ["AAA"]))
        e.run(LineInFile(f1, "AAA"))
        e.cleanup(e.RAISE)
        del e
        assert contents(f1) == "PRE\nAAA\nPOST\n"

    # check that removing the LineInFile doesn't destroy the contents added by
    # BlockInFile
    e = Engine(cfgpath)
    e.run(bif(f1, ["AAA"]))
    e.cleanup(e.RAISE)
    del e
    assert contents(f1) == "PRE\nAAA\nPOST\n"

    # put both things back in ...
    e = Engine(cfgpath)
    e.run(bif(f1, ["AAA"]))
    e.run(LineInFile(f1, "AAA"))
    e.cleanup(e.RAISE)
    del e
    assert contents(f1) == "PRE\nAAA\nPOST\n"

    # test that the LineInFile keeps itself there after removing the
    # BlockInFile
    e = Engine(cfgpath)
    e.run(LineInFile(f1, "AAA"))
    e.cleanup(e.RAISE)
    del e
    assert contents(f1) == "AAA\n"
예제 #10
0
def test_lineinfile_cleanup_interaction(tmpdir):
    from homely._engine2 import Engine
    from homely.files import MakeDir, LineInFile

    # a temporary file where the engine can store its config
    cfgpath = gettmpfilepath(tmpdir, '.json')
    f1 = os.path.join(tmpdir, 'f1.txt')
    d1 = os.path.join(tmpdir, 'f1.txt.dir')
    d1f1 = os.path.join(d1, 'f-1.txt')
    d2 = os.path.join(tmpdir, 'dir2')
    d2f1 = os.path.join(d2, 'f-1.txt')
    d3 = os.path.join(tmpdir, 'dir3')
    d3d1 = os.path.join(d3, 'sub-1')
    d3d2 = os.path.join(d3, 'sub-2')
    d3d1f1 = os.path.join(d3d1, 'somefile.txt')
    d3d2f1 = os.path.join(d3d2, 'somefile.txt')

    # make the dir and the file, make sure they both exist
    e = Engine(cfgpath)
    e.run(LineInFile(f1, "AAA"))
    e.run(MakeDir(d1))
    e.cleanup(e.RAISE)
    del e
    assert os.path.isdir(d1)
    assert contents(f1) == "AAA\n"

    # remake the engine without the dir, make sure the dir isn't kept around by
    # the file
    e = Engine(cfgpath)
    e.run(LineInFile(f1, "AAA"))
    e.cleanup(e.POSTPONE)
    del e
    assert not os.path.exists(d1)
    assert contents(f1) == "AAA\n"

    # make a new file in the directory, make sure they take ownership of the
    # directory
    e = Engine(cfgpath)
    e.run(MakeDir(d1))
    e.run(LineInFile(d1f1, "AAA"))
    e.run(LineInFile(d1f1, "BBB"))
    e.cleanup(e.POSTPONE)
    del e
    # the directory should still exist, the old file should not, and the new
    # file should contain both lines
    assert os.path.isdir(d1)
    assert not os.path.exists(f1)
    assert contents(d1f1) == "AAA\nBBB\n"

    # if we do the same thing again but without the MakeDir, we should get
    # exactly the same result
    e = Engine(cfgpath)
    e.run(LineInFile(d1f1, "AAA"))
    e.run(LineInFile(d1f1, "BBB"))
    e.cleanup(e.POSTPONE)
    del e
    # the directory should still exist, the old file should not, and the new
    # file should contain both lines
    assert os.path.isdir(d1)
    assert not os.path.exists(f1)
    assert contents(d1f1) == "AAA\nBBB\n"

    # the important thing to note is that the engine still knows that it needs
    # to clean up the directory later
    e = Engine(cfgpath)
    assert e.pathstoclean()[d1] == e.TYPE_FOLDER_ONLY
    del e

    # if we get rid of one of the LineInFile() items, the file and dir still
    # hang around
    e = Engine(cfgpath)
    e.run(LineInFile(d1f1, "AAA"))
    e.cleanup(e.POSTPONE)
    del e
    assert contents(d1f1) == "AAA\n"

    # now, when we get rid of the last LineInFile() items, everything will be
    # cleaned up
    e = Engine(cfgpath)
    e.cleanup(e.RAISE)
    del e
    assert not os.path.exists(d1)
    os.unlink(cfgpath)

    # if we make a dir ourselves, a LineInFile() will not clean it up
    assert not os.path.exists(d2)
    os.mkdir(d2)
    e = Engine(cfgpath)
    e.run(LineInFile(d2f1, "AAA"))
    del e
    assert contents(d2f1) == "AAA\n"
    e = Engine(cfgpath)
    e.cleanup(e.RAISE)
    del e
    assert os.path.isdir(d2) and not os.path.exists(d2f1)
    os.rmdir(d2)

    # if we make a file ourselves, LineInFile() will not try to clean it up
    assert not os.path.exists(f1)
    contents(f1, "")
    e = Engine(cfgpath)
    e.run(LineInFile(f1, "AAA"))
    del e
    assert contents(f1) == "AAA\n"
    e = Engine(cfgpath)
    e.cleanup(e.RAISE)
    del e
    assert contents(f1) == ""

    # if two LineInFile() items share a parent dir (even the parent dir's
    # parent, and so on), and that parent dir was made by a MakeDir(), then it
    # gets cleaned up eventually when both of the LineInFile() items go away
    # - create the folder structure
    assert not os.path.exists(d3)
    os.unlink(cfgpath)
    e = Engine(cfgpath)
    e.run(MakeDir(d3))
    e.run(MakeDir(d3d1))
    e.run(MakeDir(d3d2))
    e.cleanup(e.RAISE)
    del e
    assert all(map(os.path.isdir, [d3, d3d1, d3d2]))
    assert not any(map(os.path.exists, [d3d1f1, d3d2f1]))
    # - scrap the folder structure, but make files that depend on those folders
    e = Engine(cfgpath)
    e.run(LineInFile(d3d1f1, "AAA"))
    e.run(LineInFile(d3d2f1, "BBB"))
    e.cleanup(e.POSTPONE)
    del e
    assert contents(d3d1f1) == "AAA\n" and contents(d3d2f1) == "BBB\n"
    # - scrap the first file, and check that only things needed for the 2nd
    # file remain
    e = Engine(cfgpath)
    e.run(LineInFile(d3d2f1, "BBB"))
    e.cleanup(e.POSTPONE)
    del e
    assert not os.path.exists(d3d1)
    assert contents(d3d2f1) == "BBB\n"
    # - scrap the 2nd file and make sure everything else disappears
    e = Engine(cfgpath)
    e.cleanup(e.RAISE)
    del e
    assert not os.path.exists(d3)
예제 #11
0
def test_lineinfile_usage(tmpdir):
    from homely._engine2 import Engine
    from homely.files import WHERE_END, WHERE_TOP, LineInFile

    cfgpath = gettmpfilepath(tmpdir, '.json')

    f1 = gettmpfilepath(tmpdir)
    # make sure the following types of input raise exceptions
    bad = [
        "",  # empty line
        "something\n",  # a string containing a newline
        "   \t",  # a line containing nothing but whitespace
    ]
    for b in bad:
        try:
            LineInFile(f1, b)
        except Exception:
            # we got an exception
            pass
        else:
            raise Exception("LineInFile should not accept input %s" % repr(b))

    # make sure LineInFile() doesn't override an existing line
    contents(f1, "AAA\nBBB\n")
    e = Engine(cfgpath)
    e.run(LineInFile(f1, "BBB"))
    assert contents(f1) == "AAA\nBBB\n"

    # make sure LineInFile() puts a new line at the end of the file by default
    e = Engine(cfgpath)
    e.run(LineInFile(f1, "CCC"))
    assert contents(f1) == "AAA\nBBB\nCCC\n"

    # make sure LineInFile() doesn't move a line unnecessarily
    e = Engine(cfgpath)
    e.run(LineInFile(f1, "BBB"))
    assert contents(f1) == "AAA\nBBB\nCCC\n"

    # make sure LineInFile() can move a line to the TOP of the file
    e = Engine(cfgpath)
    e.run(LineInFile(f1, "BBB", WHERE_TOP))
    assert contents(f1) == "BBB\nAAA\nCCC\n"

    # make sure LineInFile() can move a line to the END of the file
    e = Engine(cfgpath)
    e.run(LineInFile(f1, "BBB", WHERE_END))
    assert contents(f1) == "AAA\nCCC\nBBB\n"

    # make sure LineInFile() doesn't blow away empty lines
    # - adding to end of file
    contents(f1, "\n\n", strip=False)
    e = Engine(cfgpath)
    e.run(LineInFile(f1, "AAA"))
    assert contents(f1) == "\n\nAAA\n"
    # - adding to start of file
    contents(f1, "\n\n", strip=False)
    e = Engine(cfgpath)
    e.run(LineInFile(f1, "AAA", WHERE_TOP))
    assert contents(f1) == "AAA\n\n\n"
    # - when restoring the file
    e = Engine(cfgpath)
    e.cleanup(e.RAISE)
    del e
    assert contents(f1) == "\n\n"
    # - replacing something in the middle
    contents(f1, "\n\nAAA\n\n\n", strip=False)
    e = Engine(cfgpath)
    e.run(LineInFile(f1, "AAA"))
    assert contents(f1) == "\n\nAAA\n\n\n"

    # make sure LineInFile() respects existing line endings
    # NOTE: in python2 we always use Universal Newlines when reading the file,
    # which tricks us into using "\n" when writing the file
    if sys.version_info[0] > 2:
        # - windows
        contents(f1, "AAA\r\nBBB\r\n")
        e = Engine(cfgpath)
        e.run(LineInFile(f1, "CCC"))
        assert contents(f1) == "AAA\r\nBBB\r\nCCC\r\n"
        # - mac
        contents(f1, "AAA\rBBB\r")
        e = Engine(cfgpath)
        e.run(LineInFile(f1, "BBB", WHERE_TOP))
        assert contents(f1) == "BBB\rAAA\r"

    # make sure a file that starts empty is left empty after cleanup
    contents(f1, "")
    e = Engine(cfgpath)
    e.run(LineInFile(f1, "AAA"))
    assert contents(f1) == "AAA\n"
    e = Engine(cfgpath)
    e.cleanup(e.RAISE)
    assert contents(f1) == ""