diff options
| author | Brianna Rainey | 2026-01-22 16:29:46 -0500 |
|---|---|---|
| committer | GitHub | 2026-01-22 16:29:46 -0500 |
| commit | a12be862e22bdec6a243a3f0b5f4f28d69084a2a (patch) | |
| tree | 7f2b21f58cf54deb81bfe77d7ef45358c80454f0 /tests | |
| parent | 36760579a0ae604074034c4b78cda2e3f3b001de (diff) | |
fix #89 with Image component v2.0 + 23 tests (#90)
* qtbot is needed in any test that uses a QObject
previously these tests would fail if they ran before qtbot was initialized by another test. I'm now running tests in a random order
* add tests for drawBars, readAudioFile, BlankFrame
* replace numpy.seterr with numpy.errstate
* fix incorrect comment
* add MockVideoWorker and imageDataSum
* test further into visualization (less likely to be a false positive)
* test FloodFrame function
* add failing test for Image component
one step towards fixing #89
* test component name CLI parsing
* prevent log warning when 1 setting changed
* correct tests to use widgets when needed
* test undo and blockSignals
* remove stretch_scale (use scale only)
* image ignores scale if stretch checkbox checked
fixes #89
* test Title Text component, ffmpeg command
* Image v2: replace stretched setting with resizeMode
3 resize modes are scale, cover, and stretch. Scale only applies when resizeMode is set to scale. Cover uses ImageOps.fit() to stretch while maintaining aspect ratio. Also, spinBox_scale was moved to be underneath comboBox_resizeMode.
* change transformData into staticmethod
the purpose is to allow easier reuse in other components
* add respondToAudio option to Image component
this causes the image to scale up and down slightly based on the input audio file
* cache static portion of image when animating
increases rendering speed of a 1-minute video by 12 seconds (based on two manual tests anyway)
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/__init__.py | 50 | ||||
| -rw-r--r-- | tests/test_classic_visualizer.py | 71 | ||||
| -rw-r--r-- | tests/test_commandline_parser.py | 21 | ||||
| -rw-r--r-- | tests/test_image_comp.py | 50 | ||||
| -rw-r--r-- | tests/test_mainwindow_undostack.py | 73 | ||||
| -rw-r--r-- | tests/test_text_comp.py | 32 | ||||
| -rw-r--r-- | tests/test_toolkit_common.py | 13 | ||||
| -rw-r--r-- | tests/test_toolkit_ffmpeg.py | 64 | ||||
| -rw-r--r-- | tests/test_toolkit_frame.py | 14 |
9 files changed, 364 insertions, 24 deletions
diff --git a/tests/__init__.py b/tests/__init__.py index d0073ef..b615681 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,27 +1,39 @@ -import pytest import os -import sys +import numpy + +# core always has to be imported first +import avp.core +from avp.toolkit.ffmpeg import readAudioFile +from pytest import fixture + + +@fixture +def audioData(): + """Fixture that gives a tuple of (completeAudioArray, duration)""" + soundFile = getTestDataPath("test.ogg") + yield readAudioFile(soundFile, MockVideoWorker()) def getTestDataPath(filename): + """Get path to a file in the ./data directory""" tests_dir = os.path.dirname(os.path.abspath(__file__)) return os.path.join(tests_dir, "data", filename) -def run(logFile): - """Run Pytest, which then imports and runs all tests in this module.""" - os.environ["PYTEST_QT_API"] = "PyQt6" - with open(logFile, "w") as f: - # temporarily redirect stdout to a text file so we capture pytest's output - sys.stdout = f - try: - val = pytest.main( - [ - os.path.dirname(__file__), - "-s", # disable pytest's internal capturing of stdout etc. - ] - ) - finally: - sys.stdout = sys.__stdout__ - - return val +class MockSignal: + """Pretends to be a pyqtSignal""" + + def emit(self, *args): + pass + + +class MockVideoWorker: + """Pretends to be a video thread worker""" + + progressBarSetText = MockSignal() + progressBarUpdate = MockSignal() + + +def imageDataSum(image): + """Get sum of raw data of a Pillow Image object""" + return numpy.asarray(image, dtype="int32").sum(dtype="int32") diff --git a/tests/test_classic_visualizer.py b/tests/test_classic_visualizer.py new file mode 100644 index 0000000..e301263 --- /dev/null +++ b/tests/test_classic_visualizer.py @@ -0,0 +1,71 @@ +from avp.command import Command +from pytestqt import qtbot +from pytest import fixture +from . import audioData, MockSignal, imageDataSum + + +sampleSize = 1470 # 44100 / 30 = 1470 + + +@fixture +def coreWithClassicComp(qtbot): + """Fixture providing a Command object with Classic Visualizer component added""" + command = Command() + command.core.insertComponent( + 0, command.core.moduleIndexFor("Classic Visualizer"), command + ) + yield command.core + + +def test_comp_classic_added(coreWithClassicComp): + """Add Classic Visualizer to core""" + assert len(coreWithClassicComp.selectedComponents) == 1 + + +def test_comp_classic_removed(coreWithClassicComp): + """Remove Classic Visualizer from core""" + coreWithClassicComp.removeComponent(0) + assert len(coreWithClassicComp.selectedComponents) == 0 + + +def test_comp_classic_drawBars(coreWithClassicComp, audioData): + """Call drawBars after creating audio spectrum data manually.""" + + spectrumArray = { + 0: coreWithClassicComp.selectedComponents[0].transformData( + 0, audioData[0], sampleSize, 0.08, 0.8, None, 20 + ) + } + for i in range(sampleSize, len(audioData[0]), sampleSize): + spectrumArray[i] = coreWithClassicComp.selectedComponents[0].transformData( + i, + audioData[0], + sampleSize, + 0.08, + 0.8, + spectrumArray[i - sampleSize].copy(), + 20, + ) + image = coreWithClassicComp.selectedComponents[0].drawBars( + 1920, 1080, spectrumArray[sampleSize * 4], (0, 0, 0), 0 + ) + assert imageDataSum(image) == 37872316 + + +def test_comp_classic_drawBars_using_preFrameRender(coreWithClassicComp, audioData): + """Call drawBars after creating audio spectrum data using preFrameRender.""" + comp = coreWithClassicComp.selectedComponents[0] + comp.preFrameRender( + completeAudioArray=audioData[0], + sampleSize=sampleSize, + progressBarSetText=MockSignal(), + progressBarUpdate=MockSignal(), + ) + image = comp.drawBars( + 1920, + 1080, + coreWithClassicComp.selectedComponents[0].spectrumArray[sampleSize * 4], + (0, 0, 0), + 0, + ) + assert imageDataSum(image) == 37872316 diff --git a/tests/test_commandline_parser.py b/tests/test_commandline_parser.py index 8b07b8c..d092072 100644 --- a/tests/test_commandline_parser.py +++ b/tests/test_commandline_parser.py @@ -1,37 +1,38 @@ import sys import pytest from avp.command import Command +from pytestqt import qtbot -def test_commandline_help(): +def test_commandline_help(qtbot): command = Command() sys.argv = ["", "--help"] with pytest.raises(SystemExit): command.parseArgs() -def test_commandline_help_if_bad_args(): +def test_commandline_help_if_bad_args(qtbot): command = Command() sys.argv = ["", "--junk"] with pytest.raises(SystemExit): command.parseArgs() -def test_commandline_launches_gui_if_verbose(): +def test_commandline_launches_gui_if_verbose(qtbot): command = Command() sys.argv = ["", "--verbose"] mode = command.parseArgs() assert mode == "GUI" -def test_commandline_launches_gui_if_verbose_with_project(): +def test_commandline_launches_gui_if_verbose_with_project(qtbot): command = Command() sys.argv = ["", "test", "--verbose"] mode = command.parseArgs() assert mode == "GUI" -def test_commandline_tries_to_export(): +def test_commandline_tries_to_export(qtbot): command = Command() didCallFunction = False @@ -43,3 +44,13 @@ def test_commandline_tries_to_export(): command.createAudioVisualization = captureFunction command.parseArgs() assert didCallFunction + + +def test_commandline_parses_classic_by_alias(qtbot): + command = Command() + assert command.parseCompName("original") == "Classic Visualizer" + + +def test_commandline_parses_conway_by_name(qtbot): + command = Command() + assert command.parseCompName("conway") == "Conway's Game of Life" diff --git a/tests/test_image_comp.py b/tests/test_image_comp.py new file mode 100644 index 0000000..a4f05e1 --- /dev/null +++ b/tests/test_image_comp.py @@ -0,0 +1,50 @@ +from avp.command import Command +from pytestqt import qtbot +from pytest import fixture +from . import audioData, MockSignal, imageDataSum, getTestDataPath + + +sampleSize = 1470 # 44100 / 30 = 1470 + + +@fixture +def coreWithImageComp(qtbot): + """Fixture providing a Command object with Image component added""" + command = Command() + command.settings.setValue("outputHeight", 1080) + command.settings.setValue("outputWidth", 1920) + command.core.insertComponent(0, command.core.moduleIndexFor("Image"), command) + yield command.core + + +def test_comp_image_set_path(coreWithImageComp): + "Set imagePath of Image component" + comp = coreWithImageComp.selectedComponents[0] + comp.imagePath = getTestDataPath("test.jpg") + image = comp.previewRender() + assert imageDataSum(image) == 463711601 + + +def test_comp_image_scale_50_1080p(coreWithImageComp): + """Image component stretches image to 50% at 1080p""" + comp = coreWithImageComp.selectedComponents[0] + comp.imagePath = getTestDataPath("test.jpg") + image = comp.previewRender() + sum = imageDataSum(image) + comp.page.spinBox_scale.setValue(50) + assert imageDataSum(comp.previewRender()) - sum / 4 < 2000 + + +def test_comp_image_scale_50_720p(coreWithImageComp): + """Image component stretches image to 50% at 720p""" + comp = coreWithImageComp.selectedComponents[0] + comp.imagePath = getTestDataPath("test.jpg") + comp.page.spinBox_scale.setValue(50) + image = comp.previewRender() + sum = imageDataSum(image) + comp.parent.settings.setValue("outputHeight", 720) + comp.parent.settings.setValue("outputWidth", 1280) + newImage = comp.previewRender() + assert image.width == 1920 + assert newImage.width == 1280 + assert imageDataSum(comp.previewRender()) == sum diff --git a/tests/test_mainwindow_undostack.py b/tests/test_mainwindow_undostack.py new file mode 100644 index 0000000..1eec1ef --- /dev/null +++ b/tests/test_mainwindow_undostack.py @@ -0,0 +1,73 @@ +from pytest import fixture +from pytestqt import qtbot +from avp.gui.mainwindow import MainWindow +from . import getTestDataPath + + +@fixture +def window(qtbot): + window = MainWindow(None, None) + qtbot.addWidget(window) + window.settings.setValue("outputWidth", 1920) + window.settings.setValue("outputHeight", 1080) + yield window + + +def test_undo_classic_visualizer_sensitivity(window, qtbot): + """Undo Classic Visualizer component sensitivity setting + should undo multiple merged actions.""" + window.core.insertComponent( + 0, window.core.moduleIndexFor("Classic Visualizer"), window + ) + comp = window.core.selectedComponents[0] + comp.imagePath = getTestDataPath("test.jpg") + for i in range(1, 100): + comp.page.spinBox_scale.setValue(i) + assert comp.scale == 99 + window.undoStack.undo() + assert comp.scale == 20 + + +def test_undo_image_scale(window, qtbot): + """Undo Image component scale setting should undo multiple merged actions.""" + window.core.insertComponent(0, window.core.moduleIndexFor("Image"), window) + comp = window.core.selectedComponents[0] + comp.imagePath = getTestDataPath("test.jpg") + comp.page.spinBox_scale.setValue(100) + for i in range(10, 401): + comp.page.spinBox_scale.setValue(i) + assert comp.scale == 400 + window.undoStack.undo() + assert comp.scale == 10 + window.undoStack.undo() + assert comp.scale == 100 + + +def test_undo_image_resizeMode(window, qtbot): + window.core.insertComponent(0, window.core.moduleIndexFor("Image"), window) + comp = window.core.selectedComponents[0] + comp.page.comboBox_resizeMode.setCurrentIndex(1) + assert not comp.page.spinBox_scale.isEnabled() + window.undoStack.undo() + assert comp.page.spinBox_scale.isEnabled() + + +def test_undo_title_text_merged(window, qtbot): + """Undoing title text change should undo all recent changes.""" + window.core.insertComponent(0, window.core.moduleIndexFor("Title Text"), window) + comp = window.core.selectedComponents[0] + comp.page.lineEdit_title.setText("avp") + comp.page.lineEdit_title.setText("test") + window.undoStack.undo() + assert comp.title == "Text" + + +def test_undo_title_text_not_merged(window, qtbot): + """Undoing title text change should undo up to previous different action""" + window.core.insertComponent(0, window.core.moduleIndexFor("Title Text"), window) + comp = window.core.selectedComponents[0] + comp.page.lineEdit_title.setText("avp") + comp.page.spinBox_xTextAlign.setValue(0) + comp.page.lineEdit_title.setText("test") + window.undoStack.undo() + assert comp.title == "avp" diff --git a/tests/test_text_comp.py b/tests/test_text_comp.py new file mode 100644 index 0000000..3bc0be6 --- /dev/null +++ b/tests/test_text_comp.py @@ -0,0 +1,32 @@ +from avp.command import Command +from pytestqt import qtbot +from pytest import fixture +from . import audioData, MockSignal, imageDataSum + + +@fixture +def coreWithTextComp(qtbot): + """Fixture providing a Command object with Title Text component added""" + command = Command() + command.core.insertComponent(0, command.core.moduleIndexFor("Title Text"), command) + yield command.core + + +def test_comp_text_renderFrame_resize(coreWithTextComp): + """Call renderFrame of Title Text component added to Command object.""" + comp = coreWithTextComp.selectedComponents[0] + comp.parent.settings.setValue("outputWidth", 1920) + comp.parent.settings.setValue("outputHeight", 1080) + comp.parent.core.updateComponent(0) + image = comp.frameRender(0) + assert imageDataSum(image) == 2957069 + + +def test_comp_text_renderFrame(coreWithTextComp): + """Call renderFrame of Title Text component added to Command object.""" + comp = coreWithTextComp.selectedComponents[0] + comp.parent.settings.setValue("outputWidth", 1280) + comp.parent.settings.setValue("outputHeight", 720) + comp.parent.core.updateComponent(0) + image = comp.frameRender(0) + assert imageDataSum(image) == 1412293 or 1379298 diff --git a/tests/test_toolkit_common.py b/tests/test_toolkit_common.py new file mode 100644 index 0000000..d903842 --- /dev/null +++ b/tests/test_toolkit_common.py @@ -0,0 +1,13 @@ +from pytestqt import qtbot +from avp.command import Command +from avp.toolkit import blockSignals + + +def test_blockSignals(qtbot): + command = Command() + command.core.insertComponent(0, 0, command) + comp = command.core.selectedComponents[0] + assert comp.page.spinBox_scale.signalsBlocked() == False + with blockSignals(comp.page.spinBox_scale): + assert comp.page.spinBox_scale.signalsBlocked() == True + assert comp.page.spinBox_scale.signalsBlocked() == False diff --git a/tests/test_toolkit_ffmpeg.py b/tests/test_toolkit_ffmpeg.py new file mode 100644 index 0000000..b015470 --- /dev/null +++ b/tests/test_toolkit_ffmpeg.py @@ -0,0 +1,64 @@ +import pytest +from avp.command import Command +from avp.toolkit.ffmpeg import createFfmpegCommand +from . import audioData + + +def test_readAudioFile_data(audioData): + assert len(audioData[0]) == 218453 + + +def test_readAudioFile_duration(audioData): + assert audioData[1] == 3.95 + + +@pytest.mark.parametrize("width, height", ((1920, 1080), (1280, 720))) +def test_createFfmpegCommand(width, height): + command = Command() + command.settings.setValue("outputWidth", width) + command.settings.setValue("outputHeight", height) + ffmpegCmd = createFfmpegCommand("test.ogg", "/tmp", command.core.selectedComponents) + assert ffmpegCmd == [ + "ffmpeg", + "-thread_queue_size", + "512", + "-y", + "-f", + "rawvideo", + "-vcodec", + "rawvideo", + "-s", + "%sx%s" % (width, height), + "-pix_fmt", + "rgba", + "-r", + "30", + "-t", + "0.100", + "-an", + "-i", + "-", + "-t", + "0.100", + "-i", + "test.ogg", + "-map", + "0:v", + "-map", + "1:a", + "-vcodec", + "libx264", + "-acodec", + "aac", + "-b:v", + "2500k", + "-b:a", + "192k", + "-pix_fmt", + "yuv420p", + "-preset", + "medium", + "-f", + "mp4", + "/tmp", + ] diff --git a/tests/test_toolkit_frame.py b/tests/test_toolkit_frame.py new file mode 100644 index 0000000..9486227 --- /dev/null +++ b/tests/test_toolkit_frame.py @@ -0,0 +1,14 @@ +import numpy +from avp.toolkit.frame import BlankFrame, FloodFrame + + +def test_blank_frame(): + """BlankFrame creates a frame of all zeros""" + assert numpy.asarray(BlankFrame(1920, 1080), dtype="int32").sum() == 0 + + +def test_flood_frame(): + """FloodFrame given (1, 1, 1, 1) creates a frame of sum 1920 * 1080 * 4""" + assert numpy.asarray(FloodFrame(1920, 1080, (1, 1, 1, 1)), dtype="int32").sum() == ( + 1920 * 1080 * 4 + ) |
