Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure that tests do not leave any files behind #5229

Closed
snejus opened this issue May 7, 2024 · 12 comments · Fixed by #5345
Closed

Ensure that tests do not leave any files behind #5229

snejus opened this issue May 7, 2024 · 12 comments · Fixed by #5345
Labels
refactor testing Relates to the testing/CI infrastructure

Comments

@snejus
Copy link
Member

snejus commented May 7, 2024

I have been running tests across the codebase frequently these days and have just discovered thousands of files in my /tmp directory. For example, I checked which tests from test/plugins directory leave files behind:

for fi in test/plugins/*.py; do 
  print Testing $fi && pytest $fi &>/dev/null
  drs=(/tmp/tmp*)
  if (( $#drs )); then 
    print -l $drs && rm -r $drs
  fi
done
Testing test/plugins/__init__.py
Testing test/plugins/lyrics_download_samples.py
Testing test/plugins/test_acousticbrainz.py
Testing test/plugins/test_advancedrewrite.py
Testing test/plugins/test_albumtypes.py
Testing test/plugins/test_art.py
/tmp/tmp2gwz0iin.jpg
/tmp/tmp7gxr676p.jpg
/tmp/tmp88ypd9a4.png
/tmp/tmpc9_dhkt1.jpg
/tmp/tmpjlro_7kl.png
/tmp/tmprehbg2uh.jpg
/tmp/tmps37w1k7g.jpg
/usr/bin/rm: remove 7 arguments recursively? y
removed '/tmp/tmp2gwz0iin.jpg'
removed '/tmp/tmp7gxr676p.jpg'
removed '/tmp/tmp88ypd9a4.png'
removed '/tmp/tmpc9_dhkt1.jpg'
removed '/tmp/tmpjlro_7kl.png'
removed '/tmp/tmprehbg2uh.jpg'
removed '/tmp/tmps37w1k7g.jpg'
Testing test/plugins/test_bareasc.py
/tmp/tmposwaq51j
/tmp/tmpqdoitx3p
/tmp/tmpqj57zdnm
/usr/bin/rm: remove 3 arguments recursively? y
removed directory '/tmp/tmposwaq51j/libdir'
removed directory '/tmp/tmposwaq51j'
removed directory '/tmp/tmpqdoitx3p/libdir'
removed directory '/tmp/tmpqdoitx3p'
removed directory '/tmp/tmpqj57zdnm/libdir'
removed directory '/tmp/tmpqj57zdnm'
Testing test/plugins/test_beatport.py
Testing test/plugins/test_bucket.py
Testing test/plugins/test_convert.py
Testing test/plugins/test_discogs.py
Testing test/plugins/test_edit.py
Testing test/plugins/test_embedart.py
/tmp/tmp04pf5orx
/tmp/tmp0p3zrmvr
/tmp/tmp1c_13z5i
/tmp/tmp1iwishs_.jpg
/tmp/tmpfl7et0y5
/tmp/tmpfu9vpdi9
/tmp/tmph5to5ktl
/tmp/tmpiemkfj5o
/tmp/tmpn__of7he
/tmp/tmpp1hnf3dh
/tmp/tmppumez7dg
/tmp/tmpqxlwwq42.jpg
/tmp/tmpr__5fn99
/tmp/tmptks_h0e7
/tmp/tmpv7f27emo
/tmp/tmpvp2regn7
/tmp/tmpw8dq5diz
/tmp/tmpxy4kl2i8
/usr/bin/rm: remove 18 arguments recursively? y
removed directory '/tmp/tmp04pf5orx'
removed directory '/tmp/tmp0p3zrmvr'
removed directory '/tmp/tmp1c_13z5i'
removed '/tmp/tmp1iwishs_.jpg'
removed directory '/tmp/tmpfl7et0y5'
removed directory '/tmp/tmpfu9vpdi9'
removed directory '/tmp/tmph5to5ktl'
removed directory '/tmp/tmpiemkfj5o'
removed directory '/tmp/tmpn__of7he'
removed directory '/tmp/tmpp1hnf3dh'
removed directory '/tmp/tmppumez7dg'
removed '/tmp/tmpqxlwwq42.jpg'
removed directory '/tmp/tmpr__5fn99'
removed directory '/tmp/tmptks_h0e7'
removed directory '/tmp/tmpv7f27emo'
removed directory '/tmp/tmpvp2regn7'
removed directory '/tmp/tmpw8dq5diz'
removed directory '/tmp/tmpxy4kl2i8'
Testing test/plugins/test_embyupdate.py
Testing test/plugins/test_export.py
Testing test/plugins/test_fetchart.py
Testing test/plugins/test_filefilter.py
Testing test/plugins/test_ftintitle.py
Testing test/plugins/test_hook.py
Testing test/plugins/test_ihate.py
Testing test/plugins/test_importadded.py
Testing test/plugins/test_importfeeds.py
Testing test/plugins/test_info.py
Testing test/plugins/test_ipfs.py
Testing test/plugins/test_keyfinder.py
Testing test/plugins/test_lastgenre.py
Testing test/plugins/test_limit.py
Testing test/plugins/test_lyrics.py
Testing test/plugins/test_mbsubmit.py
Testing test/plugins/test_mbsync.py
Testing test/plugins/test_mpdstats.py
Testing test/plugins/test_parentwork.py
Testing test/plugins/test_permissions.py
Testing test/plugins/test_player.py
Testing test/plugins/test_playlist.py
Testing test/plugins/test_play.py
/tmp/tmp0on9a_ru.m3u
/tmp/tmp2lnwq43r.m3u
/tmp/tmp2rjt0boe.m3u
/tmp/tmp3tenj5ru.m3u
/tmp/tmp_6aguiwh.m3u
/tmp/tmp8ggf8znt.m3u
/tmp/tmpel0a12lq.m3u
/tmp/tmpwd0g39m_.m3u
/tmp/tmpwxpl0eas.m3u
/tmp/tmpxzytwbdp.m3u
/usr/bin/rm: remove 10 arguments recursively? y
removed '/tmp/tmp0on9a_ru.m3u'
removed '/tmp/tmp2lnwq43r.m3u'
removed '/tmp/tmp2rjt0boe.m3u'
removed '/tmp/tmp3tenj5ru.m3u'
removed '/tmp/tmp_6aguiwh.m3u'
removed '/tmp/tmp8ggf8znt.m3u'
removed '/tmp/tmpel0a12lq.m3u'
removed '/tmp/tmpwd0g39m_.m3u'
removed '/tmp/tmpwxpl0eas.m3u'
removed '/tmp/tmpxzytwbdp.m3u'
Testing test/plugins/test_plexupdate.py
Testing test/plugins/test_plugin_mediafield.py
Testing test/plugins/test_random.py
Testing test/plugins/test_replaygain.py
Testing test/plugins/test_smartplaylist.py
Testing test/plugins/test_spotify.py
Testing test/plugins/test_subsonicupdate.py
Testing test/plugins/test_the.py
Testing test/plugins/test_thumbnails.py
Testing test/plugins/test_types_plugin.py
Testing test/plugins/test_web.py
Testing test/plugins/test_zero.py
/tmp/tmp4jxe4d30
/usr/bin/rm: remove 1 argument recursively? y
removed '/tmp/tmp4jxe4d30'
@snejus snejus added refactor testing Relates to the testing/CI infrastructure labels May 7, 2024
@snejus snejus changed the title Ensure that tests do not any files behind Ensure that tests do not leave any files behind May 7, 2024
@khrystianc
Copy link

I would like to take a shot at resolving this if it has not already been addressed. Please let me know

@snejus
Copy link
Member Author

snejus commented May 27, 2024

I would like to take a shot at resolving this if it has not already been addressed. Please let me know

@khrystianc go ahead!!!

@khrystianc
Copy link

Submitted the PR in hopes to fix this issue. I was not able to fully recreate it, but I have added code into the files that looked to be creating unnecessary tmp files.

#5285

@khrystianc
Copy link

If my PR does not resolve your issue, the following code can be run in a file to handle tmp file cleanup as well:

import os
import glob

def cleanup_temp_files():
    temp_files = glob.glob('/tmp/testfile*')  # Adjust the pattern to match your temporary test files
    for temp_file in temp_files:
        try:
            os.remove(temp_file)
            print(f'Removed: {temp_file}')
        except OSError as e:
            print(f'Error deleting {temp_file}: {e}')

if __name__ == '__main__':
    cleanup_temp_files()

@bal-e
Copy link
Member

bal-e commented Jun 25, 2024

I'm very interested in improving the testing infrastructure for all of beets. In particular, I think a better handling of temporary files -- how they are named and created, not just their removal -- would allow us to run tests in parallel, which should significantly cut down the latency of tests locally and on CI. It's a major change, but I'm wondering whether there would be interest in moving in that direction.

@snejus
Copy link
Member Author

snejus commented Jun 26, 2024

@bal-e, consider installing pytest-xdist and try running poe test -n4 - you should see a significant speedup :)

@bal-e
Copy link
Member

bal-e commented Jun 26, 2024

Yeah, I just discovered pytest-xdist is a thing, and it looks great! I'm thinking of making a big PR for transitioning beets from unittest to pytest (the library not just the util), which would greatly simplify assertions and also let us implement tempdir cleanup really well.

@bal-e
Copy link
Member

bal-e commented Jun 26, 2024

@snejus I don't know what the specs of the CI runner look like, but using xdist on there could speed stuff up too. It's worth a try, but I think we first need to make sure that the tests don't rely on global state (which is again much easier using pytest fixtures).

@snejus
Copy link
Member Author

snejus commented Jul 1, 2024

@bal-e reagrding this issue specifically, I spent a couple of days looking into these tests that write temp files. In those (easy) cases where tests are responsible for cleaning up, helper.TestHelper.teardown_beets function is available which makes life easy. I can see this being transformed into a pytest context-based fixture which cleans up automatically. For now, though, it's enough to make sure the teardown is appropriately called. Unfortunately, the sad reality is that only the test_bareasc.py module can be fixed this way.

If you have a look at fetchart.py, embedart.py and artresizer.py, you will find that they create temporary files in isolation which aren't that easy to track down for cleanup. In this case the issue lies in the functionality/implementation rather than the test setup, so it's a bit more complicated to fix.

I will soon submit a pull request that addresses the above.

@snejus
Copy link
Member Author

snejus commented Jul 1, 2024

Regarding migration to pytest - I will love seeing this happening, even if that's going to be a monumental task. I think we'd want to think about how can we achieve this iteratively, for example:

  1. Deduplicate existing tests, namely functionality in beets.test._common.TestCase and beets.helper.TestHelper, making sure we have just a single implementation of that functionality
  2. Replace unittest-specific things like unittest.skip by pytest.mark.skip and such.
  3. Replace unittest-specific assert methods by assert calls, like assertEqual(a, b) by assert a == b. I remember working on a sed script that helps with this:
      s/self\.assertTrue.([^)]+)./assert \1/
      s/self\.assertFalse.([^)]+)./assert not \1/
      s/self\.assertIsNone.(.*)\)(, |$)/assert \1 is None/
      s/self\.assertIsNotNone.(.*)\)(, |$)/assert \1 is not None/
      s/self\.assertEqual.([^,]+), (.*).$/assert \1 == \2/
      s/self\.assertNotIn.([^,]+), (.*).$/assert \1 not in \2/
      s/self\.assertIn.([^,]+), (.*).$/assert \1 in \2/
      s/self\.assertGreater.([^,]+), (.*).$/assert \1 > \2/
      s/self\.assertNotEqual.([^,]+), (.*).$/assert \1 != \2/
      s/self\.assertExists.([^)]+)./assert Path(\1).exists()/
      s/self\.assertNotExists.([^)]+)./assert not Path(\1).exists()/
      s/assertIsNone.(.+).$/\1/
    Of course it needs to be updated regarding tests in beets.

Once we've replaced all unittest specifics with pytest equivalents (the changes above do not require adjusting the tests structure!), then, I think, we should be able to go ahead with replacing setUp and tearDown implementations with pytest fixtures.

Otherwise, if we go ahead with migrating to pytest fixtures right away, I think we're risking the PRs becoming large with loads of changes in them. Of course, this doesn't apply to tests that have limited usage of unittest - those can be migrated easily I think.

@snejus
Copy link
Member Author

snejus commented Jul 1, 2024

@bal-e there's actually one test module that already uses pytest: see plugins/test_aura.py

@snejus
Copy link
Member Author

snejus commented Jul 2, 2024

See #5345

snejus added a commit that referenced this issue Jul 18, 2024
Fixes #5229, is part of #5361 and relates to #5285.

I have to admit thsi was a fairly tough task - I initially assumed that
the problem lies
with how the tests are setup, and that we're probably missing some
`teardown_beets` calls
here and there.

Unfortunately, it was not so simple. I came across several issues that
gave rise to
leftover temporary files:

1. `fetchart`, `artresizer` and `play` handling of temporary files.
These plugins created
isolated temporary files outside of the directories that tests clean up.
You will find
I added a couple of functions (namely `get_module_tempdir`) that force
these plugins to
create files in directories determined by their module names. This way
we can clean up
   after them using the new `CleanupModulesMixin`.

2. Tests that ran temporary directories setup twice, running
`_common.TestCase.setUp` and
`test.helper.TestHelper.setup_beets`. Both of these ran `self.temp_dir =
mkdtemp()`,
therefore the directories created by the initial setup persisted since
those have been
overridden and thus unreachable in the teardown. Here, I removed the
`setUp` calls, see

   - `test/plugins/test_embedart.py`
   - `test/test_importer.py`
   - and `test/test_plugins.py` where `setup_beets` was called twice

3. `test/test_config_command.py` attempted to manage the temporary
directory by itself,
where I found that `tearDown` failed to remove the directory for four
tests. Could not
figure out the cause, and found that delegating this task to
`TestHelper` fixed the
   issue.

4. Mediafile fixture removal depended on calling
`remove_mediafile_fixtures` method, which
`test/plugins/test_zero.py` failed to do. I made the fixtures to be
created within the
same `temp_dir` directory that gets removed in the teardown, so now they
are taken care
   of automatically.

In summary, see the test modules that left files behind:

```
Temp files created by test/__init__.py
Temp files created by test/plugins/__init__.py
Temp files created by test/plugins/lyrics_download_samples.py
Temp files created by test/plugins/test_acousticbrainz.py
Temp files created by test/plugins/test_advancedrewrite.py
Temp files created by test/plugins/test_albumtypes.py
Temp files created by test/plugins/test_art.py
	/tmp/tmp11nicahe.jpg
	/tmp/tmp1bjmodum.png
	/tmp/tmped7nhls4.jpg
	/tmp/tmpflnzr9wz.jpg
	/tmp/tmpjngkauqs.png
	/tmp/tmpkzy9mn6t.jpg
	/tmp/tmpph_wmuea.jpg
	/tmp/tmps6gk58i_.jpg
	/tmp/tmpz2eji_o4.jpg
Temp files created by test/plugins/test_aura.py
Temp files created by test/plugins/test_bareasc.py
	/tmp/tmphl3kzhug
	/tmp/tmpnh2q6v02
	/tmp/tmpppw5qrhz
Temp files created by test/plugins/test_beatport.py
Temp files created by test/plugins/test_bucket.py
Temp files created by test/plugins/test_convert.py
Temp files created by test/plugins/test_discogs.py
Temp files created by test/plugins/test_edit.py
Temp files created by test/plugins/test_embedart.py
	/tmp/tmp1ayvqzhx
	/tmp/tmp58k6mdfx.jpg
	/tmp/tmp64c2lqiv
	/tmp/tmp6nar4kr5
	/tmp/tmp6u0d5dex
	/tmp/tmpacoq7w_f
	/tmp/tmpajnr_sxr
	/tmp/tmpasj16beh
	/tmp/tmpboyaixb5
	/tmp/tmpcrmcyt5r
	/tmp/tmpdomje5g3
	/tmp/tmplu3o6t6g
	/tmp/tmpns_xvkns
	/tmp/tmpo87o1h6o.jpg
	/tmp/tmpqem39h_j
	/tmp/tmprlzm18pb
	/tmp/tmpt22v4u6x
	/tmp/tmptp3rxdgv
Temp files created by test/plugins/test_embyupdate.py
Temp files created by test/plugins/test_export.py
Temp files created by test/plugins/test_fetchart.py
Temp files created by test/plugins/test_filefilter.py
Temp files created by test/plugins/test_ftintitle.py
Temp files created by test/plugins/test_hook.py
Temp files created by test/plugins/test_ihate.py
Temp files created by test/plugins/test_importadded.py
Temp files created by test/plugins/test_importfeeds.py
Temp files created by test/plugins/test_info.py
Temp files created by test/plugins/test_ipfs.py
Temp files created by test/plugins/test_keyfinder.py
Temp files created by test/plugins/test_lastgenre.py
Temp files created by test/plugins/test_limit.py
Temp files created by test/plugins/test_lyrics.py
Temp files created by test/plugins/test_mbsubmit.py
Temp files created by test/plugins/test_mbsync.py
Temp files created by test/plugins/test_mpdstats.py
Temp files created by test/plugins/test_parentwork.py
Temp files created by test/plugins/test_permissions.py
Temp files created by test/plugins/test_player.py
Temp files created by test/plugins/test_playlist.py
Temp files created by test/plugins/test_play.py
	/tmp/tmp6ohknmve.m3u
	/tmp/tmp8rw2z_j4.m3u
	/tmp/tmp9vi27ypx.m3u
	/tmp/tmpa_s66jh8.m3u
	/tmp/tmpb7h3cn3n.m3u
	/tmp/tmpexbmqvry.m3u
	/tmp/tmpinbqrt80.m3u
	/tmp/tmpql02hax5.m3u
	/tmp/tmpvbdzprsf.m3u
	/tmp/tmpzipim36x.m3u
Temp files created by test/plugins/test_plexupdate.py
Temp files created by test/plugins/test_plugin_mediafield.py
Temp files created by test/plugins/test_random.py
Temp files created by test/plugins/test_replaygain.py
Temp files created by test/plugins/test_smartplaylist.py
Temp files created by test/plugins/test_spotify.py
Temp files created by test/plugins/test_subsonicupdate.py
Temp files created by test/plugins/test_the.py
Temp files created by test/plugins/test_thumbnails.py
Temp files created by test/plugins/test_types_plugin.py
Temp files created by test/plugins/test_web.py
Temp files created by test/plugins/test_zero.py
	/tmp/tmp3ub9xmzy
Temp files created by test/rsrc/beetsplug/test.py
Temp files created by test/rsrc/convert_stub.py
Temp files created by test/testall.py
Temp files created by test/test_art_resize.py
	/tmp/tmp3p7p60ih.jpg
	/tmp/tmp8exclgit.jpg
	/tmp/tmpkrrjsitl.jpg
	/tmp/tmpw6n8ee8e.jpg
	/tmp/tmpygws_0aw.jpg
Temp files created by test/test_autotag.py
Temp files created by test/test_config_command.py
	/tmp/tmp333f0r2j
	/tmp/tmphr356z5r
	/tmp/tmporp4rag2
	/tmp/tmpy7sjqdsw
Temp files created by test/test_datequery.py
Temp files created by test/test_dbcore.py
Temp files created by test/test_files.py
Temp files created by test/test_hidden.py
Temp files created by test/test_importer.py
	/tmp/tmp0m363gfb
	/tmp/tmp2n3i13mc
	/tmp/tmpxk3v304s
Temp files created by test/test_library.py
Temp files created by test/test_logging.py
Temp files created by test/test_m3ufile.py
Temp files created by test/test_mb.py
Temp files created by test/test_metasync.py
Temp files created by test/test_pipeline.py
Temp files created by test/test_plugins.py
	/tmp/tmp6pxhx67u
	/tmp/tmpb8pqi9ui
	/tmp/tmpcx_658g7
	/tmp/tmp_giqb9jz
	/tmp/tmpgm9xk94_
	/tmp/tmpk60l6bt3
	/tmp/tmpqoj4la68
	/tmp/tmptcdu20rp
	/tmp/tmpvr7k5shn
	/tmp/tmpwnfnzs91
Temp files created by test/test_query.py
Temp files created by test/test_sort.py
Temp files created by test/test_template.py
Temp files created by test/test_ui_commands.py
	/tmp/tmpns2u94w6
Temp files created by test/test_ui_importer.py
Temp files created by test/test_ui_init.py
Temp files created by test/test_ui.py
Temp files created by test/test_util.py
Temp files created by test/test_vfs.py
```

And that's what we have right now:

```
Temp files created by test/__init__.py
Temp files created by test/plugins/__init__.py
Temp files created by test/plugins/lyrics_download_samples.py
Temp files created by test/plugins/test_acousticbrainz.py
Temp files created by test/plugins/test_advancedrewrite.py
Temp files created by test/plugins/test_albumtypes.py
Temp files created by test/plugins/test_art.py
Temp files created by test/plugins/test_aura.py
Temp files created by test/plugins/test_bareasc.py
Temp files created by test/plugins/test_beatport.py
Temp files created by test/plugins/test_bucket.py
Temp files created by test/plugins/test_convert.py
Temp files created by test/plugins/test_discogs.py
Temp files created by test/plugins/test_edit.py
Temp files created by test/plugins/test_embedart.py
Temp files created by test/plugins/test_embyupdate.py
Temp files created by test/plugins/test_export.py
Temp files created by test/plugins/test_fetchart.py
Temp files created by test/plugins/test_filefilter.py
Temp files created by test/plugins/test_ftintitle.py
Temp files created by test/plugins/test_hook.py
Temp files created by test/plugins/test_ihate.py
Temp files created by test/plugins/test_importadded.py
Temp files created by test/plugins/test_importfeeds.py
Temp files created by test/plugins/test_info.py
Temp files created by test/plugins/test_ipfs.py
Temp files created by test/plugins/test_keyfinder.py
Temp files created by test/plugins/test_lastgenre.py
Temp files created by test/plugins/test_limit.py
Temp files created by test/plugins/test_lyrics.py
Temp files created by test/plugins/test_mbsubmit.py
Temp files created by test/plugins/test_mbsync.py
Temp files created by test/plugins/test_mpdstats.py
Temp files created by test/plugins/test_parentwork.py
Temp files created by test/plugins/test_permissions.py
Temp files created by test/plugins/test_player.py
Temp files created by test/plugins/test_playlist.py
Temp files created by test/plugins/test_play.py
Temp files created by test/plugins/test_plexupdate.py
Temp files created by test/plugins/test_plugin_mediafield.py
Temp files created by test/plugins/test_random.py
Temp files created by test/plugins/test_replaygain.py
Temp files created by test/plugins/test_smartplaylist.py
Temp files created by test/plugins/test_spotify.py
Temp files created by test/plugins/test_subsonicupdate.py
Temp files created by test/plugins/test_the.py
Temp files created by test/plugins/test_thumbnails.py
Temp files created by test/plugins/test_types_plugin.py
Temp files created by test/plugins/test_web.py
Temp files created by test/plugins/test_zero.py
Temp files created by test/rsrc/beetsplug/test.py
Temp files created by test/rsrc/convert_stub.py
Temp files created by test/testall.py
Temp files created by test/test_art_resize.py
Temp files created by test/test_autotag.py
Temp files created by test/test_config_command.py
Temp files created by test/test_datequery.py
Temp files created by test/test_dbcore.py
Temp files created by test/test_files.py
Temp files created by test/test_hidden.py
Temp files created by test/test_importer.py
Temp files created by test/test_library.py
Temp files created by test/test_logging.py
Temp files created by test/test_m3ufile.py
Temp files created by test/test_mb.py
Temp files created by test/test_metasync.py
Temp files created by test/test_pipeline.py
Temp files created by test/test_plugins.py
Temp files created by test/test_query.py
Temp files created by test/test_sort.py
Temp files created by test/test_template.py
Temp files created by test/test_ui_commands.py
Temp files created by test/test_ui_importer.py
Temp files created by test/test_ui_init.py
Temp files created by test/test_ui.py
Temp files created by test/test_util.py
Temp files created by test/test_vfs.py
```

Note that the command which provides the output is now available through
`poe`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
refactor testing Relates to the testing/CI infrastructure
Projects
None yet
3 participants