diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index 47b7fcb..449d4be 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -41,6 +41,11 @@ jobs: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_NO_SUBFOLDERS=1 cmake --build build --config Release + # Run tests + - name: Tests + run: | + ctest --test-dir build --build-config Release + # Gather documentation and executables to a common directory # Ignore copying errors from only one of build/jpegoptim or build/jpegoptim.exe existing - name: Prepare files diff --git a/.gitignore b/.gitignore index f38b007..f4d1d93 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,5 @@ jpegoptim.prn jpegoptim.ps jpegoptim.txt *.o -*.jpg build/ autom4te.cache/ diff --git a/CMakeLists.txt b/CMakeLists.txt index a42ba8d..2798b6a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -447,3 +447,17 @@ if(ARITH_ENABLED) else() message(STATUS "Arithmetic Coding: Disabled") endif() + + + +find_package(Python3 COMPONENTS Interpreter Development) + +if (Python3_FOUND) + enable_testing() + add_test(NAME unittests + COMMAND ${Python3_EXECUTABLE} test.py + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/test + ) + + set_property(TEST unittests PROPERTY ENVIRONMENT "JPEGOPTIM=$") +endif() diff --git a/Makefile.in b/Makefile.in index 9e3b30f..a238cae 100644 --- a/Makefile.in +++ b/Makefile.in @@ -43,6 +43,7 @@ LDFLAGS = @LDFLAGS@ LIBS = @LIBS@ STRIP = strip +PYTHON ?= python3 INSTALL = @INSTALL@ INSTALL_DATA = @INSTALL_DATA@ @@ -58,6 +59,8 @@ DISTNAME = $(PKGNAME)-$(Version) OBJS = $(PKGNAME).o jpegdest.o jpegsrc.o jpegmarker.o misc.o @GNUGETOPT@ +.PHONY: test + all: $(PKGNAME) dssim.o: dssim.c @@ -107,4 +110,7 @@ love: spell: codespell -S .git -S tools +test: all + (cd test && $(PYTHON) test.py -v) + # eof diff --git a/test/README b/test/README new file mode 100644 index 0000000..acb6cde --- /dev/null +++ b/test/README @@ -0,0 +1,13 @@ +Test images for jpegoptim. + +Copyright 2023 by Timo Kokkonen. +These images are licensed under CC BY-NC-SA 4.0. + +To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/ + + +jpegoptim_test1.jpg: Test image created with Photoshop using generative AI. +jpegoptim_test2.jpg: Test image scaled down and already optimized. + + + diff --git a/test/jpegoptim_test1.jpg b/test/jpegoptim_test1.jpg new file mode 100644 index 0000000..ce8ec6e Binary files /dev/null and b/test/jpegoptim_test1.jpg differ diff --git a/test/jpegoptim_test2-broken.jpg b/test/jpegoptim_test2-broken.jpg new file mode 100644 index 0000000..f77fced Binary files /dev/null and b/test/jpegoptim_test2-broken.jpg differ diff --git a/test/jpegoptim_test2.jpg b/test/jpegoptim_test2.jpg new file mode 100644 index 0000000..ee0a3fd Binary files /dev/null and b/test/jpegoptim_test2.jpg differ diff --git a/test/test.py b/test/test.py new file mode 100755 index 0000000..d8ce86f --- /dev/null +++ b/test/test.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# +# test.py -- Unit tests for jpegoptim +# +# Copyright (C) 2023 Timo Kokkonen +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +"""jpegoptim unit tester""" + +import os +import subprocess +import unittest + + +class JpegoptimTests(unittest.TestCase): + """jpegoptim test cases""" + + program = '../jpegoptim' + debug = False + + def setUp(self): + if "JPEGOPTIM" in os.environ: + self.program = os.environ["JPEGOPTIM"] + + def run_test(self, args, check=True, directory=None): + """execute jpegoptim for a test""" + command = [self.program] + args + if directory: + if not os.path.isdir(directory): + os.makedirs(directory) + command.extend(['-o', '-d', directory]) + if self.debug: + print(f'\nRun command: {" ".join(command)}') + res = subprocess.run(command, encoding="utf-8", check=check, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + output = res.stdout + if self.debug: + print(f'Result: {res.returncode}') + print(f'---\n{output}\n---\n') + return output, res.returncode + + + def test_version(self): + """test version information output""" + output, _ = self.run_test(['--version']) + self.assertIn('GNU General Public License', output) + self.assertRegex(output, r'jpegoptim v\d+\.\d+\.\d+') + + def test_noarguments(self): + """test running withouth arguments""" + output, res = self.run_test([], check=False) + self.assertEqual(1, res) + self.assertRegex(output, r'file argument\(?s\)? missing') + + def test_default(self): + """test default optimization""" + output, _ = self.run_test(['jpegoptim_test1.jpg'], directory='tmp/default') + self.assertRegex(output, r'\s\[OK\]\s.*\soptimized\.\s*$') + # check that output file is indeed smaller than the input file + self.assertGreater(os.path.getsize('jpegoptim_test1.jpg'), + os.path.getsize('tmp/default/jpegoptim_test1.jpg')) + + # check that output file is valid and "optimized" + output, _ = self.run_test(['-n', 'tmp/default/jpegoptim_test1.jpg']) + self.assertRegex(output, r'\s\[OK\]\s.*\sskipped\.\s*$') + + def test_lossy(self): + """test lossy optimization""" + output, _ = self.run_test(['-m', '10', 'jpegoptim_test1.jpg'], + directory='tmp/lossy') + self.assertRegex(output, r'\s\[OK\]\s.*\soptimized\.\s*$') + # check that output file is indeed smaller than the input file + self.assertGreater(os.path.getsize('jpegoptim_test1.jpg'), + os.path.getsize('tmp/lossy/jpegoptim_test1.jpg')) + + # check that output file is valid and "optimized" + output, _ = self.run_test(['-n', 'tmp/lossy/jpegoptim_test1.jpg']) + self.assertRegex(output, r'\s\[OK\]\s.*\sskipped\.\s*$') + + def test_optimized(self): + """test already optimized image""" + output, _ = self.run_test(['jpegoptim_test2.jpg'], + directory='tmp/optimized') + self.assertRegex(output, r'\s\[OK\]\s.*\sskipped\.\s*$') + + def test_broken(self): + """test broken image""" + output, _ = self.run_test(['jpegoptim_test2-broken.jpg'], + directory='tmp/broken', check=False) + self.assertRegex(output, r'\s\[WARNING\]\s.*\sskipped\.\s*$') + + +if __name__ == '__main__': + unittest.main() + +# eof :-)