mirror of
https://github.com/RafaelSolVargas/Vulkan.git
synced 2025-10-29 16:57:23 +00:00
Now using own test module to test vulkan downloader methods
This commit is contained in:
@@ -92,6 +92,10 @@ To run your Bot in Heroku 24/7, you will need the Procfile located in root, then
|
|||||||
- https://github.com/xrisk/heroku-opus.git
|
- https://github.com/xrisk/heroku-opus.git
|
||||||
|
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
The tests were written manually with no package due to problems with async function in other packages, to execute them type in root: <br>
|
||||||
|
`python run_tests.py`<br>
|
||||||
|
|
||||||
## License
|
## License
|
||||||
- This program is free software: you can redistribute it and/or modify it under the terms of the [MIT License](https://github.com/RafaelSolVargas/Vulkan/blob/master/LICENSE).
|
- This program is free software: you can redistribute it and/or modify it under the terms of the [MIT License](https://github.com/RafaelSolVargas/Vulkan/blob/master/LICENSE).
|
||||||
|
|
||||||
|
|||||||
10
Tests/Colors.py
Normal file
10
Tests/Colors.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
class Colors:
|
||||||
|
HEADER = '\033[95m'
|
||||||
|
OKBLUE = '\033[94m'
|
||||||
|
OKCYAN = '\033[96m'
|
||||||
|
OKGREEN = '\033[92m'
|
||||||
|
WARNING = '\033[93m'
|
||||||
|
FAIL = '\033[91m'
|
||||||
|
ENDC = '\033[0m'
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
UNDERLINE = '\033[4m'
|
||||||
50
Tests/LoopRunner.py
Normal file
50
Tests/LoopRunner.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import asyncio
|
||||||
|
from asyncio import AbstractEventLoop
|
||||||
|
from threading import Thread
|
||||||
|
from typing import Any, Coroutine, List
|
||||||
|
|
||||||
|
|
||||||
|
class LoopRunner(Thread):
|
||||||
|
"""
|
||||||
|
Class to help deal with asyncio coroutines and loops
|
||||||
|
Copyright: https://agariinc.medium.com/advanced-strategies-for-testing-async-code-in-python-6196a032d8d7
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, loop: AbstractEventLoop) -> None:
|
||||||
|
# We ensure to always use the same loop
|
||||||
|
self.loop = loop
|
||||||
|
Thread.__init__(self, name='runner')
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
asyncio.set_event_loop(self.loop)
|
||||||
|
try:
|
||||||
|
self.loop.run_forever()
|
||||||
|
finally:
|
||||||
|
if self.loop.is_running():
|
||||||
|
self.loop.close()
|
||||||
|
|
||||||
|
def run_coroutine(self, coroutine: Coroutine) -> Any:
|
||||||
|
"""Run a coroutine inside the loop and return the result, doesn't allow concurrency"""
|
||||||
|
result = asyncio.run_coroutine_threadsafe(coroutine, self.loop)
|
||||||
|
return result.result()
|
||||||
|
|
||||||
|
def _stop(self):
|
||||||
|
self.loop.stop()
|
||||||
|
|
||||||
|
def run_in_thread(self, callback, *args):
|
||||||
|
return self.loop.call_soon_threadsafe(callback, *args)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
return self.run_in_thread(self._stop)
|
||||||
|
|
||||||
|
def run_coroutines_list(self, coroutineList: List[Coroutine]) -> None:
|
||||||
|
"""Create multiple tasks in the loop and wait for them, use concurrency"""
|
||||||
|
tasks = []
|
||||||
|
for coroutine in coroutineList:
|
||||||
|
tasks.append(self.loop.create_task(coroutine))
|
||||||
|
|
||||||
|
self.run_coroutine(self.__waitForMultipleTasks(tasks))
|
||||||
|
|
||||||
|
async def __waitForMultipleTasks(self, coroutines: List[Coroutine]) -> None:
|
||||||
|
"""Function to trigger the await for asyncio.wait coroutines"""
|
||||||
|
await asyncio.wait(coroutines)
|
||||||
86
Tests/TestBase.py
Normal file
86
Tests/TestBase.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import asyncio
|
||||||
|
from time import time
|
||||||
|
from typing import Callable, List, Tuple
|
||||||
|
from Tests.Colors import Colors
|
||||||
|
from Music.Downloader import Downloader
|
||||||
|
from Music.Searcher import Searcher
|
||||||
|
from Tests.TestsHelper import TestsConstants
|
||||||
|
from Tests.LoopRunner import LoopRunner
|
||||||
|
|
||||||
|
|
||||||
|
class VulkanTesterBase:
|
||||||
|
"""My own module to execute asyncio tests"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._downloader = Downloader()
|
||||||
|
self._searcher = Searcher()
|
||||||
|
self._constants = TestsConstants()
|
||||||
|
# Get the list of methods objects of this class if start with test
|
||||||
|
self._methodsList: List[Callable] = [getattr(self, func) for func in dir(self) if callable(
|
||||||
|
getattr(self, func)) and func.startswith("test")]
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
self.__printSeparator()
|
||||||
|
methodsSummary: List[Tuple[Callable, bool]] = []
|
||||||
|
testsSuccessQuant = 0
|
||||||
|
testsStartTime = time()
|
||||||
|
|
||||||
|
for method in self._methodsList:
|
||||||
|
currentTestStartTime = time()
|
||||||
|
self.__printTestStart(method)
|
||||||
|
success = False
|
||||||
|
try:
|
||||||
|
self._setUp()
|
||||||
|
success = method()
|
||||||
|
except Exception as e:
|
||||||
|
success = False
|
||||||
|
print(f'ERROR -> {e}')
|
||||||
|
finally:
|
||||||
|
self._tearDown()
|
||||||
|
|
||||||
|
methodsSummary.append((method, success))
|
||||||
|
runTime = time() - currentTestStartTime # Get the run time of the current test
|
||||||
|
if success:
|
||||||
|
testsSuccessQuant += 1
|
||||||
|
self.__printTestSuccess(method, runTime)
|
||||||
|
else:
|
||||||
|
self.__printTestFailure(method, runTime)
|
||||||
|
|
||||||
|
self.__printSeparator()
|
||||||
|
|
||||||
|
testsRunTime = time() - testsStartTime
|
||||||
|
self.__printTestsSummary(methodsSummary, testsSuccessQuant, testsRunTime)
|
||||||
|
|
||||||
|
def _setUp(self) -> None:
|
||||||
|
self._runner = LoopRunner(asyncio.new_event_loop())
|
||||||
|
self._runner.start()
|
||||||
|
|
||||||
|
def _tearDown(self) -> None:
|
||||||
|
self._runner.stop()
|
||||||
|
self._runner.join()
|
||||||
|
|
||||||
|
def __printTestsSummary(self, methods: List[Tuple[Callable, bool]], totalSuccess: int, runTime: int) -> None:
|
||||||
|
for index, methodResult in enumerate(methods):
|
||||||
|
method = methodResult[0]
|
||||||
|
success = methodResult[1]
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print(f'{Colors.OKGREEN} {index} -> {method.__name__} = Success {Colors.ENDC}')
|
||||||
|
else:
|
||||||
|
print(f'{Colors.FAIL} {index} -> {method.__name__} = Failed {Colors.ENDC}')
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(
|
||||||
|
f'TESTS EXECUTED: {len(methods)} | SUCCESS: {totalSuccess} | FAILED: {len(methods) - totalSuccess} | TIME: {runTime:.2f}sec')
|
||||||
|
|
||||||
|
def __printTestStart(self, method: Callable) -> None:
|
||||||
|
print(f'🧪 - Starting {method.__name__}')
|
||||||
|
|
||||||
|
def __printTestSuccess(self, method: Callable, runTime: int) -> None:
|
||||||
|
print(f'{method.__name__} -> {Colors.OKGREEN} Success {Colors.ENDC} | ⏰ - {runTime:.2f}sec')
|
||||||
|
|
||||||
|
def __printTestFailure(self, method: Callable, runTime: int) -> None:
|
||||||
|
print(f'{method.__name__} -> {Colors.FAIL} Test Failed {Colors.ENDC} | ⏰ - {runTime:.2f}sec')
|
||||||
|
|
||||||
|
def __printSeparator(self) -> None:
|
||||||
|
print('=-=' * 15)
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from typing import List
|
|
||||||
import unittest
|
|
||||||
from Music.Downloader import Downloader
|
|
||||||
from Music.Searcher import Searcher
|
|
||||||
from Music.Playlist import Playlist
|
|
||||||
from Music.Song import Song
|
|
||||||
from Tests.TestsHelper import TestsConstants
|
|
||||||
|
|
||||||
|
|
||||||
def myAsyncTest(coro):
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
loop = asyncio.new_event_loop()
|
|
||||||
asyncio.set_event_loop(None)
|
|
||||||
try:
|
|
||||||
return loop.run_until_complete(coro(*args, **kwargs))
|
|
||||||
finally:
|
|
||||||
loop.close()
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
class TestDownloader(unittest.IsolatedAsyncioTestCase):
|
|
||||||
def __init__(self, methodName: str = ...) -> None:
|
|
||||||
self.downloader = Downloader()
|
|
||||||
self.searcher = Searcher()
|
|
||||||
self.constants = TestsConstants()
|
|
||||||
super().__init__(methodName)
|
|
||||||
|
|
||||||
@myAsyncTest
|
|
||||||
async def test_emptyString(self):
|
|
||||||
musicsList = await self.downloader.extract_info('')
|
|
||||||
|
|
||||||
self.assertEqual(musicsList, [], self.constants.EMPTY_STRING_ERROR_MSG)
|
|
||||||
|
|
||||||
@myAsyncTest
|
|
||||||
async def test_YoutubeMusicUrl(self) -> None:
|
|
||||||
musics = await self.searcher.search(self.constants.YT_MUSIC_URL)
|
|
||||||
|
|
||||||
self.assertTrue(len(musics) > 0)
|
|
||||||
|
|
||||||
@myAsyncTest
|
|
||||||
async def test_YoutubePersonalPlaylist(self) -> None:
|
|
||||||
musics = await self.searcher.search(self.constants.YT_PERSONAL_PLAYLIST_URL)
|
|
||||||
|
|
||||||
self.assertTrue(len(musics) > 0)
|
|
||||||
|
|
||||||
@myAsyncTest
|
|
||||||
async def test_YoutubeChannelPlaylist(self) -> None:
|
|
||||||
# Search the link to determine names
|
|
||||||
musicsInfo = await self.searcher.search(self.constants.YT_CHANNEL_PLAYLIST_URL)
|
|
||||||
|
|
||||||
self.assertTrue(len(musicsInfo) > 0)
|
|
||||||
|
|
||||||
# Create and store songs in list
|
|
||||||
playlist = Playlist()
|
|
||||||
songsList = []
|
|
||||||
for info in musicsInfo:
|
|
||||||
song = Song(identifier=info, playlist=playlist, requester='')
|
|
||||||
playlist.add_song(song)
|
|
||||||
songsList.append(song)
|
|
||||||
|
|
||||||
# We need to trigger and wait multiple tasks, so we create multiple tasks with asyncio
|
|
||||||
# and then we await for each one of them. We use this because download_song is a Coroutine
|
|
||||||
tasks: List[asyncio.Task] = []
|
|
||||||
for song in songsList:
|
|
||||||
task = asyncio.create_task(self.downloader.download_song(song))
|
|
||||||
tasks.append(task)
|
|
||||||
|
|
||||||
# Await for each task to finish
|
|
||||||
for task in tasks:
|
|
||||||
await task
|
|
||||||
|
|
||||||
self.assertTrue(self.__verifySuccessfullyPreload(songsList))
|
|
||||||
|
|
||||||
@myAsyncTest
|
|
||||||
async def test_YoutubeMixPlaylist(self) -> None:
|
|
||||||
music = await self.searcher.search(self.constants.YT_MIX_URL)
|
|
||||||
|
|
||||||
# Musics from Mix should download only the first music
|
|
||||||
self.assertTrue(len(music) == 1)
|
|
||||||
|
|
||||||
@myAsyncTest
|
|
||||||
async def test_musicTitle(self):
|
|
||||||
playlist = Playlist()
|
|
||||||
song = Song(self.constants.MUSIC_TITLE_STRING, playlist, '')
|
|
||||||
playlist.add_song(song)
|
|
||||||
|
|
||||||
task = asyncio.create_task(self.downloader.download_song(song))
|
|
||||||
await task
|
|
||||||
|
|
||||||
self.assertFalse(song.problematic)
|
|
||||||
|
|
||||||
def __verifySuccessfullyPreload(self, songs: List[Song]) -> bool:
|
|
||||||
for song in songs:
|
|
||||||
if song.title == None:
|
|
||||||
print('Song failed to download')
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == 'main':
|
|
||||||
unittest.main()
|
|
||||||
91
Tests/VDownloaderTests.py
Normal file
91
Tests/VDownloaderTests.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
from typing import List
|
||||||
|
from Tests.TestBase import VulkanTesterBase
|
||||||
|
from Music.Playlist import Playlist
|
||||||
|
from Music.Song import Song
|
||||||
|
from asyncio import Task
|
||||||
|
|
||||||
|
|
||||||
|
class VulkanDownloaderTest(VulkanTesterBase):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def test_emptyString(self) -> bool:
|
||||||
|
musicsList = self._runner.run_coroutine(self._downloader.extract_info(''))
|
||||||
|
|
||||||
|
if musicsList == []:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_YoutubeMusicUrl(self) -> bool:
|
||||||
|
musicsList = self._runner.run_coroutine(self._searcher.search(self._constants.YT_MUSIC_URL))
|
||||||
|
|
||||||
|
if len(musicsList) > 0:
|
||||||
|
print(musicsList[0])
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_YoutubeChannelPlaylist(self) -> None:
|
||||||
|
# Search the link to determine names
|
||||||
|
musicsList = self._runner.run_coroutine(
|
||||||
|
self._searcher.search(self._constants.YT_CHANNEL_PLAYLIST_URL))
|
||||||
|
|
||||||
|
if len(musicsList) == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Create and store songs in list
|
||||||
|
playlist = Playlist()
|
||||||
|
songsList: List[Song] = []
|
||||||
|
for info in musicsList:
|
||||||
|
song = Song(identifier=info, playlist=playlist, requester='')
|
||||||
|
playlist.add_song(song)
|
||||||
|
songsList.append(song)
|
||||||
|
|
||||||
|
# Create a list of coroutines without waiting for them
|
||||||
|
tasks: List[Task] = []
|
||||||
|
for song in songsList:
|
||||||
|
tasks.append(self._downloader.download_song(song))
|
||||||
|
|
||||||
|
# Send for runner to execute them concurrently
|
||||||
|
self._runner.run_coroutines_list(tasks)
|
||||||
|
|
||||||
|
for song in songsList:
|
||||||
|
if song.problematic or song.title == None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def test_YoutubeMixPlaylist(self) -> None:
|
||||||
|
# Search the link to determine names
|
||||||
|
music = self._runner.run_coroutine(
|
||||||
|
self._searcher.search(self._constants.YT_MIX_URL))
|
||||||
|
|
||||||
|
# Musics from Mix should download only the first music
|
||||||
|
if len(music) == 1:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_musicTitle(self):
|
||||||
|
playlist = Playlist()
|
||||||
|
song = Song(self._constants.MUSIC_TITLE_STRING, playlist, '')
|
||||||
|
playlist.add_song(song)
|
||||||
|
|
||||||
|
self._runner.run_coroutine(self._downloader.download_song(song))
|
||||||
|
|
||||||
|
if song.problematic:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
print(song.title)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def test_YoutubePersonalPlaylist(self) -> None:
|
||||||
|
musics = self._runner.run_coroutine(
|
||||||
|
self._searcher.search(self._constants.YT_PERSONAL_PLAYLIST_URL))
|
||||||
|
|
||||||
|
if len(musics) > 0:
|
||||||
|
print(musics)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
5
run_tests.py
Normal file
5
run_tests.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from Tests.VDownloaderTests import VulkanDownloaderTest
|
||||||
|
|
||||||
|
|
||||||
|
tester = VulkanDownloaderTest()
|
||||||
|
tester.run()
|
||||||
Reference in New Issue
Block a user