mirror of
https://github.com/beetbox/beets.git
synced 2025-12-07 17:16:07 +01:00
* Modify existing tests in order to explicitely check for differences (or lack of) in the items fields, with the intention to ensure that no unintended changes slip through. * Added tests for modifying a single item from a list of items, and for editing the album field of the items (stub for discussing whether the actual album should be updated, etc).
226 lines
9.7 KiB
Python
226 lines
9.7 KiB
Python
# This file is part of beets.
|
|
# Copyright 2015, Adrian Sampson and Diego Moreda.
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining
|
|
# a copy of this software and associated documentation files (the
|
|
# "Software"), to deal in the Software without restriction, including
|
|
# without limitation the rights to use, copy, modify, merge, publish,
|
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
# permit persons to whom the Software is furnished to do so, subject to
|
|
# the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be
|
|
# included in all copies or substantial portions of the Software.
|
|
|
|
from __future__ import (division, absolute_import, print_function,
|
|
unicode_literals)
|
|
import codecs
|
|
|
|
from mock import Mock, patch
|
|
from test._common import unittest
|
|
from test.helper import TestHelper, control_stdin
|
|
|
|
from beets import library
|
|
from beetsplug.edit import EditPlugin
|
|
|
|
|
|
class ModifyFileMocker(object):
|
|
"""Helper for modifying a file, replacing or editing its contents. Used for
|
|
mocking the calls to the external editor during testing."""
|
|
|
|
def __init__(self, contents=None, replacements=None):
|
|
""" `self.contents` and `self.replacements` are initalized here, in
|
|
order to keep the rest of the functions of this class with the same
|
|
signature as `EditPlugin.get_editor()`, making mocking easier.
|
|
- `contents`: string with the contents of the file to be used for
|
|
`overwrite_contents()`
|
|
- `replacement`: dict with the in-place replacements to be used for
|
|
`replace_contents()`, in the form {'previous string': 'new string'}
|
|
|
|
TODO: check if it can be solved more elegantly with a decorator
|
|
"""
|
|
self.contents = contents
|
|
self.replacements = replacements
|
|
self.action = self.overwrite_contents
|
|
if replacements:
|
|
self.action = self.replace_contents
|
|
|
|
def overwrite_contents(self, filename):
|
|
"""Modify `filename`, replacing its contents with `self.contents`. If
|
|
`self.contents` is empty, the file remains unchanged.
|
|
"""
|
|
if self.contents:
|
|
with codecs.open(filename, 'w', encoding='utf8') as f:
|
|
f.write(self.contents)
|
|
|
|
def replace_contents(self, filename):
|
|
"""Modify `filename`, reading its contents and replacing the strings
|
|
specified in `self.replacements`.
|
|
"""
|
|
with codecs.open(filename, 'r', encoding='utf8') as f:
|
|
contents = f.read()
|
|
for old, new_ in self.replacements.iteritems():
|
|
contents = contents.replace(old, new_)
|
|
with codecs.open(filename, 'w', encoding='utf8') as f:
|
|
f.write(contents)
|
|
|
|
|
|
class EditCommandTest(unittest.TestCase, TestHelper):
|
|
""" Black box tests for `beetsplug.edit`. Command line interaction is
|
|
simulated using `test.helper.control_stdin()`, and yaml editing via an
|
|
external editor is simulated using `ModifyFileMocker`.
|
|
"""
|
|
ALBUM_COUNT = 1
|
|
TRACK_COUNT = 10
|
|
|
|
def setUp(self):
|
|
self.setup_beets()
|
|
self.load_plugins('edit')
|
|
# make sure that we avoid invoking the editor except for making changes
|
|
self.config['edit']['diff_method'] = ''
|
|
# add an album, storing the original fields for comparison
|
|
self.album = self.add_album_fixture(track_count=self.TRACK_COUNT)
|
|
self.album_orig = {f: self.album[f] for f in self.album._fields}
|
|
self.items_orig = [{f: item[f] for f in item._fields} for
|
|
item in self.album.items()]
|
|
|
|
# keep track of write()s
|
|
library.Item.write = Mock()
|
|
|
|
def tearDown(self):
|
|
self.teardown_beets()
|
|
self.unload_plugins()
|
|
|
|
def run_mocked_command(self, modify_file_args={}, stdin=[]):
|
|
"""Run the edit command, with mocked stdin and yaml writing."""
|
|
m = ModifyFileMocker(**modify_file_args)
|
|
with patch.object(EditPlugin, 'get_editor', side_effect=m.action):
|
|
with control_stdin('\n'.join(stdin)):
|
|
self.run_command('edit')
|
|
|
|
def assertCounts(self, album_count=ALBUM_COUNT, track_count=TRACK_COUNT,
|
|
write_call_count=TRACK_COUNT, title_starts_with=''):
|
|
"""Several common assertions on Album, Track and call counts."""
|
|
self.assertEqual(len(self.lib.albums()), album_count)
|
|
self.assertEqual(len(self.lib.items()), track_count)
|
|
self.assertEqual(library.Item.write.call_count, write_call_count)
|
|
self.assertTrue(all(i.title.startswith(title_starts_with)
|
|
for i in self.lib.items()))
|
|
|
|
def assertItemFieldsModified(self, library_items, items, fields=[]):
|
|
"""Assert that items in the library (`lib_items`) have different values
|
|
on the specified `fields` (and *only* on those fields), compared to
|
|
`items`.
|
|
An empty `fields` list results in asserting that no modifications have
|
|
been performed.
|
|
"""
|
|
changed_fields = []
|
|
for lib_item, item in zip(library_items, items):
|
|
changed_fields.append([field for field in lib_item._fields
|
|
if lib_item[field] != item[field]])
|
|
self.assertTrue(all(diff_fields == fields for diff_fields in
|
|
changed_fields))
|
|
|
|
def test_title_edit_discard(self):
|
|
"""Edit title for all items in the library, then discard changes-"""
|
|
# edit titles
|
|
self.run_mocked_command({'replacements': {u't\u00eftle':
|
|
u'modified t\u00eftle'}},
|
|
# edit? y, done? y, modify? n
|
|
['y', 'y', 'n'])
|
|
|
|
self.assertCounts(write_call_count=0,
|
|
title_starts_with=u't\u00eftle')
|
|
self.assertItemFieldsModified(self.album.items(), self.items_orig, [])
|
|
|
|
def test_title_edit_apply(self):
|
|
"""Edit title for all items in the library, then apply changes."""
|
|
# edit titles
|
|
self.run_mocked_command({'replacements': {u't\u00eftle':
|
|
u'modified t\u00eftle'}},
|
|
# edit? y, done? y, modify? y
|
|
['y', 'y', 'y'])
|
|
|
|
self.assertCounts(write_call_count=self.TRACK_COUNT,
|
|
title_starts_with=u'modified t\u00eftle')
|
|
self.assertItemFieldsModified(self.album.items(), self.items_orig,
|
|
['title'])
|
|
|
|
def test_single_title_edit_apply(self):
|
|
"""Edit title for on items in the library, then apply changes."""
|
|
# edit title
|
|
self.run_mocked_command({'replacements': {u't\u00eftle 9':
|
|
u'modified t\u00eftle 9'}},
|
|
# edit? y, done? y, modify? y
|
|
['y', 'y', 'y'])
|
|
|
|
self.assertCounts(write_call_count=1,)
|
|
# no changes except on last item
|
|
self.assertItemFieldsModified(list(self.album.items())[:-1],
|
|
self.items_orig[:-1], [])
|
|
self.assertEqual(list(self.album.items())[-1].title,
|
|
u'modified t\u00eftle 9')
|
|
|
|
def test_noedit(self):
|
|
"""Do not edit anything."""
|
|
# do not edit anything
|
|
self.run_mocked_command({'contents': None},
|
|
# edit? n -> "no changes found"
|
|
['n'])
|
|
|
|
self.assertCounts(write_call_count=0,
|
|
title_starts_with=u't\u00eftle')
|
|
self.assertItemFieldsModified(self.album.items(), self.items_orig, [])
|
|
|
|
def test_album_edit_apply(self):
|
|
"""Edit the album field for all items in the library, apply changes
|
|
|
|
TODO: decide if the plugin should be wise enough to update the album,
|
|
and handle other complex cases (create new albums, etc). At the moment
|
|
this test only checks for modifications on items.
|
|
"""
|
|
# edit album
|
|
self.run_mocked_command({'replacements': {u'\u00e4lbum':
|
|
u'modified \u00e4lbum'}},
|
|
# edit? y, done? y, modify? y
|
|
['y', 'y', 'y'])
|
|
|
|
self.assertCounts(write_call_count=self.TRACK_COUNT)
|
|
self.assertItemFieldsModified(self.album.items(), self.items_orig,
|
|
['album'])
|
|
|
|
def test_malformed_yaml_discard(self):
|
|
"""Edit the yaml file incorrectly (resulting in a malformed yaml
|
|
document), then discard changes.
|
|
|
|
TODO: this test currently fails, on purpose. User gets into an endless
|
|
"fix?" -> "ok.fixed." prompt loop unless he is able to provide a
|
|
well-formed yaml."""
|
|
# edit the yaml file to an invalid file
|
|
self.run_mocked_command({'contents': '!MALFORMED'},
|
|
# edit? n, done? y, fix? n
|
|
['y', 'y', 'n'])
|
|
|
|
self.assertCounts(write_call_count=0,
|
|
title_starts_with=u't\u00eftle')
|
|
|
|
def test_invalid_yaml_discard(self):
|
|
"""Edit the yaml file incorrectly (resulting in a well-formed but
|
|
invalid yaml document), then discard changes.
|
|
|
|
TODO: this test currently fails, on purpose. `check_diff()` chokes
|
|
ungracefully"""
|
|
# edit the yaml file to an invalid file
|
|
self.run_mocked_command({'contents': 'wellformed: yes, but invalid'},
|
|
# edit? n, done? y
|
|
['y', 'y'])
|
|
|
|
self.assertCounts(write_call_count=0,
|
|
title_starts_with=u't\u00eftle')
|
|
|
|
|
|
def suite():
|
|
return unittest.TestLoader().loadTestsFromName(__name__)
|
|
|
|
if __name__ == b'__main__':
|
|
unittest.main(defaultTest='suite')
|