# 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 self.add_album_fixture(track_count=self.TRACK_COUNT) # 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 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') 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') 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') 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')