00001 #!/usr/bin/python 00002 00003 # Copyright (C) 2009 Ferdinand Majerech 00004 # This file is part of IMGCrush 00005 # For conditions of distribution and use, see copyright notice in LICENSE.txt 00006 00007 import unittest 00008 import subprocess 00009 import re 00010 import copy 00011 import os 00012 from math import sqrt 00013 from imgcrush import globals 00014 00015 def run_bash(cmd, err=False): 00016 """Executes Bash commands and returns their output. 00017 00018 If err is true, output is returned from stderr instead of stdout. 00019 """ 00020 p = subprocess.Popen(cmd, shell = True, stdout = subprocess.PIPE, 00021 stderr = subprocess.PIPE) 00022 out, stderr = p.communicate() 00023 if err: 00024 return stderr.strip() 00025 return out.strip() 00026 00027 class TestIMGCrush(unittest.TestCase): 00028 #TODO: 0.5: make testdir a member, make teardown clear it and move 00029 # tests not using testdir to a separate class 00030 # maybe also members for path lists 00031 # init all this in setup 00032 00033 def setUp(self): 00034 """Called before each test is executed. 00035 """ 00036 pass 00037 00038 def tearDown(self): 00039 """Called after each test is executed. 00040 """ 00041 pass 00042 00043 def test_help(self): 00044 """Tests if help contains entries for all options and no redunancies. 00045 00046 Also checks if help entries show if an option requires an argument 00047 and errors with how command line options are specified, 00048 i.e. for options longer than 8 characters (imgcrush maximum) and 00049 when a short option requires an argument, checks if the long requires 00050 it as well and vice versa. 00051 """ 00052 #list containing lines of output of --help 00053 helplines = run_bash("imgcrush --help").split("\n") 00054 #offset in short options string 00055 offset = 0 00056 oshort=globals.OPTS_SHORT 00057 optsize = len(oshort) 00058 #index of current option (used to get long options from OPTS_LONG list) 00059 optnum = 0 00060 #a copy of help lines; if a help line of a command is found, it's 00061 #deleted from here so any help lines left are redunant 00062 helplinesdel = copy.deepcopy(helplines) 00063 while offset < optsize: 00064 shortopt = oshort[offset] 00065 longopt = globals.OPTS_LONG[optnum] 00066 #does this option require an argument? 00067 needsarg = False 00068 #if next character is : , this option requires an argument 00069 if offset < (optsize - 1) and oshort[offset + 1] == ":": 00070 needsarg = True 00071 self.assertTrue(longopt[-1] == "=", 00072 "Short option requires an argument but " + 00073 "the long one does not. Short option is " + 00074 shortopt + ":, long option is " + longopt) 00075 longopt = longopt[:-1] 00076 self.assertTrue(len(longopt) <= 8, "Option longer than 8" + 00077 "characters: " + longopt) 00078 found = False 00079 linere = re.compile("\s+-" + shortopt + "\s+--" + longopt) 00080 linerearg = re.compile("\s+-" + shortopt + "\s+--" + longopt + 00081 "\s+" + "val") 00082 #find help line for this option, and check if it corresponds, 00083 #delete it from helplinesdel so we know it is not redunant 00084 for line in helplines: 00085 if linere.match(line): 00086 if needsarg: 00087 self.assertTrue(linerearg.match(line), "Option " + 00088 longopt + "requires an argument but " + 00089 "help doesn't show it.") 00090 else: 00091 self.assertFalse(linerearg.match(line), "Option " + 00092 longopt + " has no argument yet " + 00093 "help shows it has.") 00094 found = True 00095 helplinesdel.remove(line) 00096 self.assertTrue(found, "Missing --help content for option " + 00097 longopt + "(" + shortopt + ")") 00098 offset += 1 00099 optnum += 1 00100 #skip the : in the short option string so we go to the next one. 00101 if needsarg: 00102 offset += 1 00103 cmdhelpre = re.compile("\s+-\w+\s+--\w+") 00104 for line in helplinesdel: 00105 self.assertFalse(cmdhelpre.match(line), 00106 "Help contains help for option that doesn't " + 00107 "exist:" + line) 00108 00109 def test_out(self): 00110 """Tests if the output directory is created and files output there. 00111 """ 00112 subprocess.call("imgcrush -pP fastest -Q 90 -o ./testdir 01.jpg 02.jpg", 00113 shell=True) 00114 paths = ["./testdir", "./testdir/01.png", "./testdir/02.png"] 00115 for path in paths: 00116 self.assertTrue(os.path.exists(path), 00117 "Output test failed: One of output paths does " + 00118 "not exist.") 00119 subprocess.call("rm -r ./testdir", shell=True) 00120 00121 def test_verbosity(self): 00122 """Tests verbosity. Checks if each higher setting spits out more text. 00123 """ 00124 qq = run_bash("imgcrush -gG fastest -qq -o ./testdir/qq 01.jpg") 00125 q = run_bash("imgcrush -gG fastest -q -o ./testdir/q 01.jpg") 00126 n = run_bash("imgcrush -gG fastest -o ./testdir/n 01.jpg") 00127 v = run_bash("imgcrush -gG fastest -v -o ./testdir/v 01.jpg") 00128 vv = run_bash("imgcrush -gG fastest -vv -o ./testdir/vv 01.jpg") 00129 allright = len(qq) <= len(q) and len(q) <= len(n) and len(n) <= len(v)\ 00130 and len(v) <= len(vv) 00131 self.assertTrue(allright, "Verbosity test failed: one of verbosity" + 00132 "options spits out more text than a higher option") 00133 subprocess.call("rm -r ./testdir", shell=True) 00134 00135 def test_multiprocessing(self): 00136 """Tests if different multiprocessing settings result in same output. 00137 """ 00138 files = ["01.jpg", "02.jpg"] 00139 fstr = " ".join(files) 00140 procs = 3 00141 filelists = [] 00142 for proc in range(0, procs): 00143 subprocess.call("imgcrush -jpgP fastest -J fastest -G fastest -m " + 00144 str(proc) + " -o ./testdir/" + str(proc) + 00145 " -M s -S 70% " + fstr, shell=True) 00146 filelists.append(os.listdir("./testdir/" + str(proc))) 00147 #assert that all filelists are identical 00148 for list in filelists: 00149 self.assertTrue(list == filelists[0], 00150 "Multiprocessing test failed: outputs of different "+ 00151 "multiprocessing settings have different filenames.") 00152 for f in range (0, len(filelists[0])): 00153 #assert that this file is identical in all lists 00154 for list in filelists: 00155 #compare this file in first list to one in this list 00156 diff = run_bash("diff " + filelists[0][f] + " " + list[f]) 00157 self.assertTrue(not diff, "Multiprocessing test failed: " + 00158 "outputs of different multiprocessing " + 00159 "settings are not identical.") 00160 subprocess.call("rm -r ./testdir", shell=True) 00161 00162 def test_out_dir(self): 00163 """Tests directory processing. 00164 """ 00165 subprocess.call("imgcrush -pP fastest -Q 90 -o ./testdir ./dir", 00166 shell=True) 00167 paths = ["dir", "dir/03.png", "dir/04.png"] 00168 for path in paths: 00169 self.assertTrue(os.path.exists("./testdir/" + path), "Directory " + 00170 "test failed: one or more output files don't exist") 00171 subprocess.call("rm -r ./testdir", shell=True) 00172 00173 def test_recursive(self): 00174 """Tests recursive processing. 00175 """ 00176 subprocess.call("imgcrush -pP fastest -Q 90 -r -o ./testdir ./dir", 00177 shell=True) 00178 paths = ["dir", "dir/03.png", "dir/04.png", "dir/subdir", 00179 "dir/subdir/05.png", "dir/subdir/06.png"] 00180 for path in paths: 00181 self.assertTrue(os.path.exists("./testdir/" + path), "Recursive " + 00182 "directory test failed: one or more output files " + 00183 "don't exist") 00184 subprocess.call("rm -r ./testdir", shell=True) 00185 00186 def test_formats(self): 00187 """Tests if format settings correlate with output formats. 00188 """ 00189 #we know that these files can be optimized according to input, i.e. 00190 #reference png or original file won't be output 00191 files = ["01.jpg", "02.jpg"] 00192 filebases = ["01", "02"] 00193 filestr = " ".join(files) 00194 #3-tuples of identifier (in output of file command), extension, 00195 #imgcrush option 00196 formats = [("JPEG", "jpg", "jJ"), ("PNG", "png", "pP"), 00197 ("GIF", "gif", "gG")] 00198 for format in formats: 00199 subprocess.call("imgcrush -" + format[2] + " fastest -M s -S 90% " + 00200 "-o ./testdir " + filestr, shell=True) 00201 for base in filebases: 00202 self.assertTrue(format[0] in run_bash("file ./testdir/" + 00203 base + "." + format[1]), 00204 "File format test failed: outputs are not of " + 00205 "specified filetype") 00206 subprocess.call("rm -r ./testdir", shell=True) 00207 00208 def test_jpg_settings(self): 00209 """Tests if images are optimized according to input JPG settings 00210 00211 Note: this test cannot detect bugs for certain, but if it detects 00212 something, it's a bug. 00213 """ 00214 files = ["01.jpg", "02.jpg"] 00215 filestr = " ".join(files) 00216 subprocess.call("imgcrush -j --j-qual 50 --j-s-fact 1x1 -M q -Q 10 " + 00217 "-o ./testdir " + filestr, shell=True) 00218 qualre = re.compile("\s*JPEG-Quality:\s*(\d+)") 00219 sampre = re.compile("\s*JPEG-Sampling-factors:\s*(\dx\d)") 00220 for file in files: 00221 id = run_bash("gm identify -verbose ./testdir/" + file).split("\n") 00222 for line in id: 00223 qualmatch = qualre.match(line) 00224 if qualmatch: 00225 quality = int(qualmatch.group(1)) 00226 self.assertTrue(quality >= 50, "JPG settings test failed:" + 00227 "output JPG quality lower than --j-qual .") 00228 sampmatch = sampre.match(line) 00229 if sampmatch: 00230 sfactor = sampmatch.group(1) 00231 self.assertTrue(sfactor == "1x1", "JPG settings test " + 00232 "failed: output sampling factor is not " + 00233 "--j-s-fact .") 00234 subprocess.call("rm -r ./testdir", shell=True) 00235 00236 def test_mode_quality(self): 00237 """Tests if quality file selection mode works correctly. 00238 """ 00239 files = ["01.jpg", "02.jpg"] 00240 fstr = " ".join(files) 00241 sminqual = 100.0 * sqrt(sqrt(sqrt(sqrt(sqrt(80.0/100))))) 00242 #3-tuples of directory suffix, unit suffix, absolute minimum quality 00243 units = [("pcnt", "80%", 80), ("subj", "80", sminqual)] 00244 numre = re.compile("\d\.?\d*") 00245 for unit in units: 00246 subprocess.call("imgcrush -jJ light -M q -Q " + unit[1] + 00247 " -o ./testdir/" + unit[0] + " " + fstr, 00248 shell = True) 00249 for f in files: 00250 comp = run_bash("gm compare -metric mae '" + f + 00251 "' './testdir/" + unit[0] + "/" + f + "'" ) 00252 for line in comp.split("\n"): 00253 if "Total" in line: 00254 #regular expression matching a positive number 00255 errorstr = numre.findall(line)[0] 00256 qual = 100.0 - 100 * float(errorstr) 00257 self.assertTrue(qual > unit[2], "Quality mode test " + 00258 "failed: " + unit[0] + " Output has " + 00259 "worse quality than required.") 00260 subprocess.call("rm -r ./testdir", shell=True) 00261 00262 def get_area(self, file): 00263 """A helper method that returns the area of given file in pixels. 00264 """ 00265 wh = run_bash("gm identify -format \"%w %h\" '" + file\ 00266 + "'").split(" ") 00267 return int(wh[0]) * int(wh[1]) 00268 00269 def test_mode_size(self): 00270 """Tests if file size file selection mode works correctly. 00271 """ 00272 files = ["01.jpg", "02.jpg"] 00273 fstr = " ".join(files) 00274 #3-tuples of directory suffix, unit+suffix, absolute maximum size 00275 units = [("pcnt", "80%", lambda f: 00276 int(run_bash("stat -c%s '" + f + "'")) * 0.8 ), 00277 ("kiB", "80kiB", lambda f: 80 * 1024), 00278 ("MiB", "0.08MiB", lambda f: 0.08 * 1024 ** 2), 00279 ("GiB", "0.00008GiB", lambda f: 0.00008 * 1024 ** 3), 00280 ("bytes", "80000", lambda f: 80000), 00281 ("bpp", "4bpp", lambda f: 4 * self.get_area(f) / 8.0)] 00282 for unit in units: 00283 subprocess.call("imgcrush -jJ light -M s -S " + unit[1] + 00284 " -o ./testdir/" + unit[0] + " " + fstr, 00285 shell = True) 00286 for f in files: 00287 size = int(run_bash("stat -c%s './testdir/" + unit[0] + "/" + 00288 f + "'")) 00289 self.assertTrue(size < unit[2](f), "Size mode test failed: " + 00290 unit[0] + " Output has larger size than max.") 00291 subprocess.call("rm -r ./testdir", shell=True) 00292 00293 if __name__ == '__main__': 00294 unittest.main()