coveralls: ## Pull Request Test Coverage Report for [Build 103](https://coveralls.io/builds/20018185)

* **0** of **0**   changed or added relevant lines in **0** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage decreased (**-1.2%**) to **98.784%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/20018185/badge)](https://coveralls.io/builds/20018185) |
| :-- | --: |
| Change from base [Build 99](https://coveralls.io/builds/20015065): |  -1.2% |
| Covered Lines: | 533 |
| Relevant Lines: | 533 |

---
##### 💛  - [Coveralls](https://coveralls.io)

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)

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.
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

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest coverage tox vulture pathlib2
apt-get update apt-get install -y gcc libdiscid0
3.7
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
cachetools==5.5.2
-e git+https://github.com/fennekki/cdparacord.git@5b45723e257fa16526607f31f461261a37360168#egg=cdparacord
certifi @ file:///croot/certifi_1671487769961/work/certifi
chardet==5.2.0
click==8.1.8
colorama==0.4.6
coverage==7.2.7
discid==1.2.0
distlib==0.3.9
exceptiongroup==1.2.2
filelock==3.12.2
importlib-metadata==6.7.0
iniconfig==2.0.0
musicbrainzngs==0.7.1
mutagen==1.47.0
packaging==24.0
pathlib2==2.3.7.post1
platformdirs==4.0.0
pluggy==1.2.0
pyproject-api==1.5.3
pytest==7.4.4
PyYAML==6.0.1
six==1.17.0
toml==0.10.2
tomli==2.0.1
tox==4.8.0
typing_extensions==4.7.1
virtualenv==20.26.6
vulture==2.9.1
zipp==3.15.0

name: cdparacord
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - cachetools==5.5.2
      - chardet==5.2.0
      - click==8.1.8
      - colorama==0.4.6
      - coverage==7.2.7
      - discid==1.2.0
      - distlib==0.3.9
      - exceptiongroup==1.2.2
      - filelock==3.12.2
      - importlib-metadata==6.7.0
      - iniconfig==2.0.0
      - musicbrainzngs==0.7.1
      - mutagen==1.47.0
      - packaging==24.0
      - pathlib2==2.3.7.post1
      - platformdirs==4.0.0
      - pluggy==1.2.0
      - pyproject-api==1.5.3
      - pytest==7.4.4
      - pyyaml==6.0.1
      - six==1.17.0
      - toml==0.10.2
      - tomli==2.0.1
      - tox==4.8.0
      - typing-extensions==4.7.1
      - virtualenv==20.26.6
      - vulture==2.9.1
      - zipp==3.15.0
prefix: /opt/conda/envs/cdparacord

tests/test_albumdata.py::test_weird_nothing_result tests/test_albumdata.py::test_select_albumdata tests/test_albumdata.py::test_from_user_input
tests/test_config.py::test_fail_to_create_config_dir tests/test_config.py::test_fail_to_open_config_file
tests/test_albumdata.py::test_cdstub_result tests/test_albumdata.py::test_disc_result tests/test_albumdata.py::test_musicbrainzerror_result tests/test_albumdata.py::test_initialise_track tests/test_albumdata.py::test_albumdata_tracks tests/test_albumdata.py::test_initialise_albumdata tests/test_albumdata.py::test_print_albumdata_80 tests/test_albumdata.py::test_print_albumdata_60 tests/test_albumdata.py::test_get_track_count tests/test_albumdata.py::test_get_no_track_count tests/test_albumdata.py::test_previous_result tests/test_albumdata.py::test_invalid_previous_result tests/test_albumdata.py::test_no_previous_result tests/test_albumdata.py::test_edit_albumdata tests/test_albumdata.py::test_generate_filename tests/test_albumdata.py::test_disc_result_no_date tests/test_config.py::test_create_config tests/test_config.py::test_get_encoder tests/test_config.py::test_fail_to_get_variable tests/test_config.py::test_read_config_file tests/test_config.py::test_read_invalid_config tests/test_config.py::test_update_config_no_unknown_keys tests/test_config.py::test_update_config_unknown_keys tests/test_config.py::test_update_config_with_none tests/test_config.py::test_update_config_quiet_ignore tests/test_config.py::test_ensure_default_encoder_keys_are_strings tests/test_config.py::test_ensure_default_postaction_keys_are_strings tests/test_rip.py::test_construct_rip tests/test_rip.py::test_rip_pipeline tests/test_rip.py::test_rip_track tests/test_rip.py::test_encode_track tests/test_rip.py::test_tag_track
.travis.yml cdparacord/albumdata.py
.travis.yml cdparacord/albumdata.py