Repository: fennekki/cdparacord

Issue:
Add branch coverage measurements
Add branch coverage (if our test system supports it) and then also ensure that we have total branch coverage in addition to total line coverage.

Comments:

Patch:
diff --git a/.travis.yml b/.travis.yml
index 25ea960..7df677a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -18,7 +18,7 @@ install:
 
 script:
   - pipenv run tox
-  - pipenv run coverage run -m py.test
+  - pipenv run coverage run --branch -m py.test
   - pipenv run coverage report -m
 
 after_success:
diff --git a/cdparacord/albumdata.py b/cdparacord/albumdata.py
index 6815658..757f422 100644
--- a/cdparacord/albumdata.py
+++ b/cdparacord/albumdata.py
@@ -181,8 +181,11 @@ class Albumdata:
                 return [cls._albumdata_from_cdstub(result['cdstub'])]
             elif 'disc' in result:
                 return cls._albumdata_from_disc(result['disc'])
-        except musicbrainzngs.MusicBrainzError:  # pragma: no cover
-            return []
+        except musicbrainzngs.MusicBrainzError:
+            pass
+        # If we hit the exception or there's *neither* cdstub *nor*
+        # disc, we get here.
+        return []
 
     @classmethod
     def _albumdata_from_previous_rip(cls, albumdata_file):
@@ -223,19 +226,24 @@ class Albumdata:
                 extract_next = True
 
     @classmethod
-    def _select_albumdata(cls, results, track_count):
+    def _select_albumdata(cls, results):
         max_width, max_height = shutil.get_terminal_size()
 
         state = 0
         while True:
             # State 0 is the first screen, other ones are the options
+
+            # There should be no way for state to escape these
+            # constraints.
+            assert 0 <= state <= len(results)
+
             if state == 0:
                 print('=' * max_width)
                 print('Albumdata sources available:')
                 for i in range(1, len(results) + 1):
                     print('{}) {}'.format(i, results[i - 1]['source']))
                 print('=' * max_width)
-            elif state <= track_count:
+            else:
                 print('Source {}: {}'.format(
                     state, results[state - 1]['source']))
                 cls._print_albumdata(results[state - 1])
@@ -309,8 +317,13 @@ class Albumdata:
                 s[key] = ''.join(
                     c for c in s[key] if unicodedata.category(c) in
                         ('Lu', 'Ll', 'Lt', 'Lm', 'Lo', 'Nd', 'Nl', 'No'))
-            elif safetyfilter != 'remove_restricted':
-                # This is the only valid option if it's nothing else
+            elif safetyfilter == 'remove_restricted':
+                # This is the only valid option if it's nothing else. We
+                # can safely pass because this filter is always applied
+                # at the end.
+                pass
+            else:
+                # Raise an error if *no* valid filter is found
                 raise AlbumdataError(
                     'Invalid safety filter {}'.format(safetyfilter))
 
@@ -457,6 +470,9 @@ class Albumdata:
 
         track_count = cls._get_track_count(deps.cdparanoia)
 
+        if track_count is None:
+            raise AlbumdataError('Could not figure out track count')
+
         # Data to be merged to the albumdata we select
         common_albumdata = {
             'discid': str(disc),
@@ -521,7 +537,7 @@ class Albumdata:
         # Actually drop results that have the wrong amount of tracks
         results = [r for r in results if r not in dropped]
 
-        selected = cls._select_albumdata(results, track_count)
+        selected = cls._select_albumdata(results)
 
         # Edit albumdata
         return cls._edit_albumdata(selected, track_count, deps.editor, config)


Test patch:
diff --git a/tests/test_albumdata.py b/tests/test_albumdata.py
index e6bb73f..8867a8c 100644
--- a/tests/test_albumdata.py
+++ b/tests/test_albumdata.py
@@ -191,7 +191,8 @@ def test_cdstub_result(monkeypatch, albumdata):
 
 def test_disc_result(monkeypatch, albumdata):
     """Test that disc result is processed correctly."""
-    monkeypatch.setattr('musicbrainzngs.get_releases_by_discid',
+    monkeypatch.setattr(
+        'musicbrainzngs.get_releases_by_discid',
         lambda x, includes: testdata_disc_result)
 
     a = albumdata.Albumdata._albumdata_from_musicbrainz('test')[0]
@@ -203,6 +204,33 @@ def test_disc_result(monkeypatch, albumdata):
     assert a['tracks'][0]['artist'] == 'Test Artist'
 
 
+def test_musicbrainzerror_result(monkeypatch, albumdata):
+    """Test that getting no MusicBrainz result at all works."""
+    def fake_get_releases(*x, **y):
+        import musicbrainzngs
+        raise musicbrainzngs.MusicBrainzError("test")
+
+    monkeypatch.setattr(
+        'musicbrainzngs.get_releases_by_discid',
+        fake_get_releases)
+    a = albumdata.Albumdata._albumdata_from_musicbrainz('test')
+    assert a == []
+
+
+def test_weird_nothing_result(monkeypatch, albumdata):
+    """Test a weird implausible MusicBrainz result.
+
+    Specifically, a case where we get neither cdstub nor disc which
+    shouldn't happen will hit its own branch that should be treated as
+    "no MusicBrainz result".
+    """
+    monkeypatch.setattr(
+        'musicbrainzngs.get_releases_by_discid',
+        lambda *x, **y: {})
+    a = albumdata.Albumdata._albumdata_from_musicbrainz('test')
+    assert a == []
+
+
 def test_initialise_track(albumdata):
     """Test that track is correctly initialised."""
     t = albumdata.Track(testdata['tracks'][0])
@@ -293,6 +321,22 @@ TOTAL   1150 [00:11.50]        (audio only)
     monkeypatch.setattr('subprocess.run', lambda *x, **y: obj)
     assert albumdata.Albumdata._get_track_count('') == 1
 
+
+def test_get_no_track_count(monkeypatch, albumdata):
+    """Test track count getting with empty cdparanoia output."""
+    class FakeProcess:
+        def check_returncode(self):
+            pass
+        
+        @property
+        def stdout(self):
+            return ''
+
+    obj = FakeProcess()
+    monkeypatch.setattr('subprocess.run', lambda *x, **y: obj)
+    assert albumdata.Albumdata._get_track_count('') == None
+
+
 def test_select_albumdata(capsys, monkeypatch, albumdata):
     """Test that the albumdata selection works as expected.
 
@@ -436,7 +480,7 @@ a: abort
             except StopIteration:
                 return 
         monkeypatch.setattr('builtins.input', fake_input)
-        albumdata.Albumdata._select_albumdata([testdata], 1)
+        albumdata.Albumdata._select_albumdata([testdata])
         out, err = capsys.readouterr()
 
         assert out == expected[test_index]
@@ -461,6 +505,13 @@ def test_invalid_previous_result(monkeypatch, albumdata):
         a = albumdata.Albumdata._albumdata_from_previous_rip('')
 
 
+def test_no_previous_result(monkeypatch, albumdata):
+    """Test that previous rip not existing works."""
+    monkeypatch.setattr('os.path.isfile', lambda *x: False)
+
+    a = albumdata.Albumdata._albumdata_from_previous_rip('')
+    assert a is None
+
 def test_from_user_input(monkeypatch, albumdata):
     monkeypatch.setattr('discid.read', lambda: 'test')
     monkeypatch.setattr('os.getuid', lambda: 1000)
@@ -495,6 +546,10 @@ def test_from_user_input(monkeypatch, albumdata):
     config.dict['reuse_albumdata'] = True
     assert albumdata.Albumdata.from_user_input(deps, config) is None
 
+    # Same config but this time previous albumdata is None
+    monkeypatch.setattr('cdparacord.albumdata.Albumdata._albumdata_from_previous_rip', lambda *x: None)
+    assert albumdata.Albumdata.from_user_input(deps, config) is None
+
     config.dict['use_musicbrainz'] = True
     config.dict['reuse_albumdata'] = False
     assert albumdata.Albumdata.from_user_input(deps, config) is None
@@ -503,6 +558,13 @@ def test_from_user_input(monkeypatch, albumdata):
     config.dict['reuse_albumdata'] = False
     assert albumdata.Albumdata.from_user_input(deps, config) is None
 
+    # It's plausible that we would get None here
+    config.dict['use_musicbrainz'] = False
+    config.dict['reuse_albumdata'] = True
+    monkeypatch.setattr('cdparacord.albumdata.Albumdata._get_track_count', lambda *x: None)
+    with pytest.raises(albumdata.AlbumdataError):
+        albumdata.Albumdata.from_user_input(deps, config)
+
 
 def test_edit_albumdata(monkeypatch, albumdata):
     """Test _edit_albumdata."""
@@ -563,6 +625,8 @@ def test_generate_filename(monkeypatch, albumdata):
     assert '!äbc-de' == albumdata.Albumdata._generate_filename(testdata, testdata['tracks'][0], 1, config)
     config.dict['safetyfilter'] = 'unicode_letternumber'
     assert 'äbcde' == albumdata.Albumdata._generate_filename(testdata, testdata['tracks'][0], 1, config)
+    config.dict['safetyfilter'] = 'remove_restricted'
+    assert '!äbc-de' == albumdata.Albumdata._generate_filename(testdata, testdata['tracks'][0], 1, config)
 
     # Test that it fails when we given an invalid filter
     config.dict['safetyfilter'] = 'fake and not real'
diff --git a/tests/test_config.py b/tests/test_config.py
index 1a85588..5c42f51 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -187,6 +187,27 @@ def test_update_config_unknown_keys(mock_temp_home, capsys):
     out, err = capsys.readouterr()
     assert err == 'Warning: Unknown configuration key invalid_key\n'
 
+
+def test_update_config_with_none(mock_temp_home, capsys):
+    from cdparacord import config
+
+    c = config.Config()
+    c.update({'keep_ripdir': None}, quiet_ignore=False)
+
+    out, err = capsys.readouterr()
+    assert err == ""
+
+
+def test_update_config_quiet_ignore(mock_temp_home, capsys):
+    from cdparacord import config
+
+    c = config.Config()
+    c.update({'invalid_key': True}, quiet_ignore=True)
+
+    out, err = capsys.readouterr()
+    assert err == ''
+
+
 def test_ensure_default_encoder_keys_are_strings(mock_temp_home):
     """Test default encoder configuration."""
     from cdparacord import config
diff --git a/tests/test_rip.py b/tests/test_rip.py
index 5bc2c85..78b6ed7 100644
--- a/tests/test_rip.py
+++ b/tests/test_rip.py
@@ -8,6 +8,7 @@ def get_fake_config():
         def __init__(self):
             self.fail_one = False
             self.fail_all = False
+            self.always_tag_albumartist = False
 
         def get(self, key):
             if key in ('post_rip', 'post_encode'):
@@ -28,7 +29,7 @@ def get_fake_config():
                 else:
                     return {'echo': ['${one_file}', '${out_file}']}
             elif key == 'always_tag_albumartist':
-                return True
+                return self.always_tag_albumartist
             else:
                 return ''
     yield FakeConfig
@@ -293,10 +294,13 @@ def test_tag_track(monkeypatch, get_fake_config):
     monkeypatch.setattr('mutagen.easyid3.EasyID3', FakeFile2)
     monkeypatch.setattr('mutagen.File', FakeFile)
 
-    loop = asyncio.new_event_loop()
-    asyncio.set_event_loop(loop)
-    loop.run_until_complete(r._tag_track(fake_track, fake_track.filename))
-    loop.close()
+    # Both skip and don't skip the albumartist branch
+    for always_tag in (True, False):
+        fake_config.always_tag_albumartist = always_tag
+        loop = asyncio.new_event_loop()
+        asyncio.set_event_loop(loop)
+        loop.run_until_complete(r._tag_track(fake_track, fake_track.filename))
+        loop.close()
 
-    # Assert we got the filename put in the dict
-    assert r._tagged_files[fake_track.filename] == fake_track.filename
+        # Assert we got the filename put in the dict
+        assert r._tagged_files[fake_track.filename] == fake_track.filename