1#!/usr/bin/env python3
2# SPDX-License-Identifier: Apache-2.0
3# -----------------------------------------------------------------------------
4# Copyright 2020-2023 Arm Limited
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may not
7# use this file except in compliance with the License. You may obtain a copy
8# of the License at:
9#
10#     http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17# -----------------------------------------------------------------------------
18"""
19The functional test runner is a set of tests that validate the ``astcenc``
20command line is correctly handled, under both valid and invalid usage
21scenarios. These tests do NOT validate the compression codec itself, beyond
22some very basic incidental usage needed to validate the command line.
23
24Due to the need to validate pixel colors in test images for both LDR and HDR
25images, these tests rely on an HDRI-enabled build of ImageMagic being available
26on the system path. To test if the version of ImageMagic on your system is
27HDRI-enabled run:
28
29    convert --version
30
31... and check that the string "HDRI" is present in the listed features.
32
33Test Tiles
34==========
35
36Some basic test images, each 8x8 texels and built up from 4 no. 4x4 texel
37constant color blocks, are used to help determine that the command line is
38being processed correctly.
39
40LDR Test Pattern
41----------------
42
43LDR images are an 8x8 image containing 4 4x4 constant color blocks. Assuming
44(0, 0) is the top left (TL), the component uncompressed block colors are:
45
46* (0, 0) TL = Black, opaque = (0.00, 0.00, 0.00, 1.00)
47* (7, 0) TR = Red, opaque   = (1.00, 0.00, 0.00, 1.00)
48* (0, 7) BL = White, opaque = (1.00, 1.00, 1.00, 1.00)
49* (7, 7) BR = Green, trans  = (0.25, 0.75, 0.00, 0.87)
50
51HDR Test Pattern
52----------------
53
54HDR images are an 8x8 image containing 4 4x4 constant color blocks. Assuming
55(0, 0) is the top left (TL), the component uncompressed block colors are:
56
57* (0, 0) TL = LDR Black, opaque = (0.00, 0.00, 0.00, 1.00)
58* (7, 0) TR = HDR Red, opaque   = (8.00, 0.00, 0.00, 1.00)
59* (0, 7) BL = HDR White, opaque = (3.98, 3.98, 3.98, 1.00)
60* (7, 7) BR = LDR Green, trans  = (0.25, 0.75, 0.00, 0.87)
61"""
62
63import argparse
64import filecmp
65import os
66import re
67import signal
68import string
69import subprocess as sp
70import sys
71import tempfile
72import time
73import unittest
74
75import numpy
76from PIL import Image
77
78import testlib.encoder as te
79import testlib.image as tli
80
81# Enable these to always write out, irrespective of test result
82ASTCENC_CLI_ALWAYS = False
83ASTCENC_LOG_ALWAYS = False
84
85# Enable these to write out on failure for positive tests
86ASTCENC_CLI_ON_ERROR = True
87ASTCENC_LOG_ON_ERROR = True
88
89# Enable these to write out on failure for negative tests
90ASTCENC_CLI_ON_ERROR_NEG = True
91ASTCENC_LOG_ON_ERROR_NEG = True
92
93# LDR test pattern
94ASTCENC_TEST_PATTERN_LDR = {
95    "TL": (0.00, 0.00, 0.00, 1.00),
96    "TR": (1.00, 0.00, 0.00, 1.00),
97    "BL": (1.00, 1.00, 1.00, 1.00),
98    "BR": (0.25, 0.75, 0.00, 0.87)
99}
100
101# HDR test pattern
102ASTCENC_TEST_PATTERN_HDR = {
103    "TL": (0.00, 0.00, 0.00, 1.00),
104    "TR": (8.00, 0.00, 0.00, 1.00),
105    "BL": (3.98, 3.98, 3.98, 1.00),
106    "BR": (0.25, 0.75, 0.00, 0.87)
107}
108
109LDR_RGB_PSNR_PATTERN = re.compile(r"\s*PSNR \(LDR-RGB\): (.*) dB")
110
111g_TestEncoder = "avx2"
112
113class CLITestBase(unittest.TestCase):
114    """
115    Command line interface base class.
116
117    These tests are designed to test the command line is handled correctly.
118    They are not detailed tests of the codec itself; only basic sanity checks
119    that some type of processing occurred are used.
120    """
121
122    def __init__(self, *args, **kwargs):
123        super().__init__(*args, **kwargs)
124
125        encoder = te.Encoder2x(g_TestEncoder)
126        self.binary = encoder.binary
127
128    def setUp(self):
129        """
130        Set up a test case.
131
132        Create a new temporary directory for output files.
133        """
134        self.tempDir = tempfile.TemporaryDirectory()
135
136    def tearDown(self):
137        """
138        Tear down a test case.
139
140        Clean up the temporary directory created for output files.
141        """
142        self.tempDir.cleanup()
143        self.tempDir = None
144
145    @staticmethod
146    def get_ref_image_path(profile, mode, image):
147        """
148        Get the path of a reference image on disk.
149
150        Args:
151            profile (str): The color profile.
152            mode (str): The type of image to load.
153            image (str): The image variant to load.
154
155        Returns:
156            str: The path to the test image file on disk.
157        """
158        nameMux = {
159            "LDR": {
160                "input": "png",
161                "comp": "astc"
162            },
163            "LDRS": {
164                "input": "png",
165                "comp": "astc"
166            },
167            "HDR": {
168                "input": "exr",
169                "comp": "astc"
170            }
171        }
172
173        assert profile in nameMux.keys()
174        assert mode in nameMux["LDR"].keys()
175
176        scriptDir = os.path.dirname(__file__)
177        fileName = "%s-%s-1x1.%s" % (profile, image, nameMux[profile][mode])
178        return os.path.join(scriptDir, "Data", fileName)
179
180    def get_tmp_image_path(self, profile, mode):
181        """
182        Get the path of a temporary output image on disk.
183
184        Temporary files are automatically cleaned up when the test tearDown
185        occurs.
186
187        Args:
188            profile (str): The color profile. "EXP" means explicit which means
189                the "mode" parameter is interpreted as a literal file
190                extension not a symbolic mode.
191            mode (str): The type of image to load.
192
193        Returns:
194            str: The path to the test image file on disk.
195        """
196        # Handle explicit mode
197        if profile == "EXP":
198            tmpFile, tmpPath = tempfile.mkstemp(mode, dir=self.tempDir.name)
199            os.close(tmpFile)
200            os.remove(tmpPath)
201            return tmpPath
202
203        # Handle symbolic modes
204        nameMux = {
205            "LDR": {
206                "comp": ".astc",
207                "decomp": ".png",
208                "bad": ".foo"
209            },
210            "LDRS": {
211                "comp": ".astc",
212                "decomp": ".png",
213                "bad": ".foo"
214            },
215            "HDR": {
216                "comp": ".astc",
217                "decomp": ".exr",
218                "bad": ".foo"
219            }
220        }
221
222        assert profile in nameMux.keys()
223        assert mode in nameMux["LDR"].keys()
224
225        suffix = nameMux[profile][mode]
226        tmpFile, tmpPath = tempfile.mkstemp(suffix, dir=self.tempDir.name)
227        os.close(tmpFile)
228        os.remove(tmpPath)
229        return tmpPath
230
231
232class CLIPTest(CLITestBase):
233    """
234    Command line interface positive tests.
235
236    These tests are designed to test the command line is handled correctly.
237    They are not detailed tests of the codec itself; only basic sanity checks
238    that some type of processing occurred are used.
239    """
240
241    def compare(self, image1, image2):
242        """
243        Utility function to compare two images.
244
245        Note that this comparison tests only decoded color values; any file
246        metadata is ignored, and encoding methods are not compared.
247
248        Args:
249            image1 (str): Path to the first image.
250            image2 (str): Path to the second image.
251
252        Returns:
253            bool: ``True` if the images are the same, ``False`` otherwise.
254        """
255        img1 = Image.open(image1)
256        img2 = Image.open(image2)
257
258        # Images must have same size
259        if img1.size != img2.size:
260            print("Size")
261            return False
262
263        # Images must have same number of color channels
264        if img1.getbands() != img2.getbands():
265            # ... unless the only different is alpha
266            self.assertEqual(img1.getbands(), ("R", "G", "B"))
267            self.assertEqual(img2.getbands(), ("R", "G", "B", "A"))
268
269            # ... and the alpha is always one
270            bands = img2.split()
271            alphaHist = bands[3].histogram()
272            self.assertEqual(sum(alphaHist[:-1]), 0)
273
274            # Generate a version of img2 without alpha
275            img2 = Image.merge("RGB", (bands[0], bands[1], bands[2]))
276
277        # Compute sum of absolute differences
278        dat1 = numpy.array(img1)
279        dat2 = numpy.array(img2)
280        sad = numpy.sum(numpy.abs(dat1 - dat2))
281
282        if sad != 0:
283            print(img1.load()[0, 0])
284            print(img2.load()[0, 0])
285
286        return sad == 0
287
288    def get_channel_rmse(self, image1, image2):
289        """
290        Get the channel-by-channel root mean square error.
291
292        Args:
293            image1 (str): Path to the first image.
294            image2 (str): Path to the second image.
295
296        Returns:
297            tuple: Tuple of floats containing the RMSE per channel.
298            None: Images could not be compared because they are different size.
299        """
300        img1 = Image.open(image1)
301        img2 = Image.open(image2)
302
303        # Images must have same size
304        if img1.size != img2.size:
305            return None
306
307        # Images must have same number of color channels
308        if img1.getbands() != img2.getbands():
309            # ... unless the only different is alpha
310            self.assertEqual(img1.getbands(), ("R", "G", "B"))
311            self.assertEqual(img2.getbands(), ("R", "G", "B", "A"))
312
313            # ... and the alpha is always one
314            bands = img2.split()
315            alphaHist = bands[3].histogram()
316            self.assertEqual(sum(alphaHist[:-1]), 0)
317
318            # Generate a version of img2 without alpha
319            img2 = Image.merge("RGB", (bands[0], bands[1], bands[2]))
320
321        # Compute root mean square error
322        img1bands = img1.split()
323        img2bands = img2.split()
324
325        rmseVals = []
326        imgBands = zip(img1bands, img2bands)
327        for img1Ch, img2Ch in imgBands:
328            imSz = numpy.prod(img1Ch.size)
329            dat1 = numpy.array(img1Ch)
330            dat2 = numpy.array(img2Ch)
331
332            sad = numpy.sum(numpy.square(dat1 - dat2))
333            mse = numpy.divide(sad, imSz)
334            rmse = numpy.sqrt(mse)
335            rmseVals.append(rmse)
336
337        return rmseVals
338
339    @staticmethod
340    def get_color_refs(mode, corners):
341        """
342        Build a set of reference colors from apriori color list.
343
344        Args:
345            mode (str): The color mode (LDR, or HDR)
346            corners (str or list): The corner or list of corners -- named TL,
347                TR, BL, and BR -- to return.
348
349        Returns:
350            tuple: The color value, if corners was a name.
351            [tuple]: List of color values, if corners was a list of names.
352        """
353        modes = {
354            "LDR": ASTCENC_TEST_PATTERN_LDR,
355            "HDR": ASTCENC_TEST_PATTERN_HDR
356        }
357
358        if isinstance(corners, str):
359            return [modes[mode][corners]]
360
361        return [modes[mode][corner] for corner in corners]
362
363    def assertColorSame(self, colorRef, colorNew, threshold=0.02, swiz=None):
364        """
365        Test if a color is the similar to a reference.
366
367        Will trigger a test failure if the colors are not within threshold.
368
369        Args:
370            colorRef (tuple): The reference color to compare with.
371            colorNew (tuple): The new color.
372            threshold (float): The allowed deviation from colorRef (ratio).
373            swiz (str): The swizzle string (4 characters from the set
374                `rgba01`), applied to the reference color.
375        """
376        self.assertEqual(len(colorRef), len(colorNew))
377
378        # Swizzle the reference color if needed
379        if swiz:
380            self.assertEqual(len(swiz), len(colorRef))
381
382            remap = {
383                "0": len(colorRef),
384                "1": len(colorRef) + 1
385            }
386
387            if len(colorRef) >= 1:
388                remap["r"] = 0
389            if len(colorRef) >= 2:
390                remap["g"] = 1
391            if len(colorRef) >= 3:
392                remap["b"] = 2
393            if len(colorRef) >= 4:
394                remap["a"] = 3
395
396            colorRefExt = list(colorRef) + [0.0, 1.0]
397            colorRef = [colorRefExt[remap[s]] for s in swiz]
398
399        for chRef, chNew in zip(colorRef, colorNew):
400            deltaMax = chRef * threshold
401            self.assertAlmostEqual(chRef, chNew, delta=deltaMax)
402
403    def exec(self, command, pattern=None):
404        """
405        Execute a positive test.
406
407        Will trigger a test failure if the subprocess return code is any value
408        other than zero.
409
410        Args:
411            command (list(str)): The command to execute.
412            pattern (re.Pattern): The regex pattern to search for, must
413                contain a single group (this is returned to the caller). The
414                test will fail if no pattern match is found.
415
416        Returns:
417            str: The stdout output of the child process, or the first group
418               from the passed regex pattern.
419        """
420        try:
421            result = sp.run(command, stdout=sp.PIPE, stderr=sp.PIPE,
422                            universal_newlines=True, check=True)
423            error = False
424        except sp.CalledProcessError as ex:
425            result = ex
426            error = True
427
428        # Emit debug logging if needed
429        if ASTCENC_CLI_ALWAYS or (error and ASTCENC_CLI_ON_ERROR):
430            # Format for shell replay
431            print("\n" + " ".join(command))
432            # Format for script command list replay
433            print("\n" + ", ".join(("\"%s\"" % x for x in command)))
434
435        if ASTCENC_LOG_ALWAYS or (error and ASTCENC_LOG_ON_ERROR):
436            print(result.stdout)
437
438        rcode = result.returncode
439
440        if rcode < 0:
441            msg = "Exec died with signal %s" % signal.Signals(-rcode).name
442            self.assertGreaterEqual(rcode, 0, msg)
443
444        if rcode > 0:
445            msg = "Exec died with application error %u" % rcode
446            self.assertEqual(rcode, 0, msg)
447
448        # If there is a regex pattern provided, then search for it
449        if pattern:
450            match = pattern.search(result.stdout)
451            self.assertIsNotNone(match)
452            return match.group(1)
453
454        return result.stdout
455
456    def test_ldr_compress(self):
457        """
458        Test basic LDR compression.
459        """
460        imIn = self.get_ref_image_path("LDR", "input", "A")
461        imOut = self.get_tmp_image_path("LDR", "comp")
462        imRef = self.get_ref_image_path("LDR", "comp", "A")
463
464        command = [self.binary, "-cl", imIn, imOut, "6x6", "-exhaustive"]
465        self.exec(command)
466        self.assertTrue(filecmp.cmp(imRef, imOut, False))
467
468    def test_srgb_compress(self):
469        """
470        Test basic LDR sRGB compression.
471        """
472        imIn = self.get_ref_image_path("LDRS", "input", "A")
473        imOut = self.get_tmp_image_path("LDRS", "comp")
474        imRef = self.get_ref_image_path("LDRS", "comp", "A")
475
476        command = [self.binary, "-cs", imIn, imOut, "6x6", "-exhaustive"]
477        self.exec(command)
478        self.assertTrue(filecmp.cmp(imRef, imOut, False))
479
480    def test_hdr_compress1(self):
481        """
482        Test basic HDR + LDR alpha compression.
483        """
484        imIn = self.get_ref_image_path("HDR", "input", "A")
485        imOut = self.get_tmp_image_path("HDR", "comp")
486        imRef = self.get_ref_image_path("HDR", "comp", "A")
487
488        command = [self.binary, "-ch", imIn, imOut, "6x6", "-exhaustive"]
489        self.exec(command)
490        self.assertTrue(filecmp.cmp(imRef, imOut, False))
491
492    def test_hdr_compress2(self):
493        """
494        Test basic HDR + HDR alpha compression.
495        """
496        imIn = self.get_ref_image_path("HDR", "input", "A")
497        imOut = self.get_tmp_image_path("HDR", "comp")
498        imRef = self.get_ref_image_path("HDR", "comp", "A")
499
500        command = [self.binary, "-cH", imIn, imOut, "6x6", "-exhaustive"]
501        self.exec(command)
502        self.assertTrue(filecmp.cmp(imRef, imOut, False))
503
504    def test_ldr_decompress(self):
505        """
506        Test basic LDR decompression.
507        """
508        imIn = self.get_ref_image_path("LDR", "comp", "A")
509        imOut = self.get_tmp_image_path("LDR", "decomp")
510        imRef = self.get_ref_image_path("LDR", "input", "A")
511
512        command = [self.binary, "-dl", imIn, imOut]
513        self.exec(command)
514        self.assertTrue(self.compare(imRef, imOut))
515
516    def test_srgb_decompress(self):
517        """
518        Test basic LDR sRGB decompression.
519        """
520        imIn = self.get_ref_image_path("LDRS", "comp", "A")
521        imOut = self.get_tmp_image_path("LDRS", "decomp")
522        imRef = self.get_ref_image_path("LDRS", "input", "A")
523
524        command = [self.binary, "-ds", imIn, imOut]
525        self.exec(command)
526        self.assertTrue(self.compare(imRef, imOut))
527
528    def test_hdr_decompress1(self):
529        """
530        Test basic HDR + LDR alpha decompression.
531        """
532        imIn = self.get_ref_image_path("HDR", "comp", "A")
533        imOut = self.get_tmp_image_path("HDR", "decomp")
534        imRef = self.get_ref_image_path("HDR", "input", "A")
535
536        command = [self.binary, "-dh", imIn, imOut]
537        self.exec(command)
538
539        colRef = tli.Image(imRef).get_colors((0, 0))
540        colOut = tli.Image(imOut).get_colors((0, 0))
541        self.assertColorSame(colRef, colOut)
542
543    def test_hdr_decompress2(self):
544        """
545        Test basic HDR + HDR alpha decompression.
546        """
547        imIn = self.get_ref_image_path("HDR", "comp", "A")
548        imOut = self.get_tmp_image_path("HDR", "decomp")
549        imRef = self.get_ref_image_path("HDR", "input", "A")
550
551        command = [self.binary, "-dH", imIn, imOut]
552        self.exec(command)
553
554        colRef = tli.Image(imRef).get_colors((0, 0))
555        colOut = tli.Image(imOut).get_colors((0, 0))
556        self.assertColorSame(colRef, colOut)
557
558    def test_ldr_roundtrip(self):
559        """
560        Test basic LDR round-trip
561        """
562        imIn = self.get_ref_image_path("LDR", "input", "A")
563        imOut = self.get_tmp_image_path("LDR", "decomp")
564
565        command = [self.binary, "-tl", imIn, imOut, "6x6", "-exhaustive"]
566        self.exec(command)
567        self.assertTrue(self.compare(imIn, imOut))
568
569    def test_srgb_roundtrip(self):
570        """
571        Test basic LDR sRGB round-trip
572        """
573        imIn = self.get_ref_image_path("LDRS", "input", "A")
574        imOut = self.get_tmp_image_path("LDRS", "decomp")
575
576        command = [self.binary, "-ts", imIn, imOut, "6x6", "-exhaustive"]
577        self.exec(command)
578        self.assertTrue(self.compare(imIn, imOut))
579
580    def test_hdr_roundtrip1(self):
581        """
582        Test basic HDR + LDR alpha round-trip.
583        """
584        imIn = self.get_ref_image_path("HDR", "input", "A")
585        imOut = self.get_tmp_image_path("HDR", "decomp")
586
587        command = [self.binary, "-th", imIn, imOut, "6x6", "-exhaustive"]
588        self.exec(command)
589        colIn = tli.Image(imIn).get_colors((0, 0))
590        colOut = tli.Image(imOut).get_colors((0, 0))
591        self.assertColorSame(colIn, colOut)
592
593    def test_hdr_roundtrip2(self):
594        """
595        Test basic HDR + HDR alpha round-trip.
596        """
597        imIn = self.get_ref_image_path("HDR", "input", "A")
598        imOut = self.get_tmp_image_path("HDR", "decomp")
599
600        command = [self.binary, "-tH", imIn, imOut, "6x6", "-exhaustive"]
601        self.exec(command)
602        colIn = tli.Image(imIn).get_colors((0, 0))
603        colOut = tli.Image(imOut).get_colors((0, 0))
604        self.assertColorSame(colIn, colOut)
605
606    def test_valid_2d_block_sizes(self):
607        """
608        Test all valid block sizes are accepted (2D images).
609        """
610        blockSizes = [
611            "4x4", "5x4", "5x5", "6x5", "6x6", "8x5", "8x6",
612            "10x5", "10x6", "8x8", "10x8", "10x10", "12x10", "12x12"
613        ]
614
615        imIn = self.get_ref_image_path("LDR", "input", "A")
616        imOut = self.get_tmp_image_path("LDR", "decomp")
617
618        for blk in blockSizes:
619            with self.subTest(blockSize=blk):
620                command = [self.binary, "-tl", imIn, imOut, blk, "-exhaustive"]
621                self.exec(command)
622                colIn = tli.Image(imIn).get_colors((0, 0))
623                colOut = tli.Image(imOut).get_colors((0, 0))
624                self.assertColorSame(colIn, colOut)
625
626    def test_valid_3d_block_sizes(self):
627        """
628        Test all valid block sizes are accepted (3D images).
629        """
630        blockSizes = [
631            "3x3x3",
632            "4x3x3", "4x4x3", "4x4x4",
633            "5x4x4", "5x5x4", "5x5x5",
634            "6x5x5", "6x6x5", "6x6x6"
635        ]
636
637        imIn = self.get_ref_image_path("LDR", "input", "A")
638        imOut = self.get_tmp_image_path("LDR", "decomp")
639
640        for blk in blockSizes:
641            with self.subTest(blockSize=blk):
642                command = [self.binary, "-tl", imIn, imOut, blk, "-exhaustive"]
643                self.exec(command)
644                colIn = tli.Image(imIn).get_colors((0, 0))
645                colOut = tli.Image(imOut).get_colors((0, 0))
646                self.assertColorSame(colIn, colOut)
647
648    def test_valid_presets(self):
649        """
650        Test all valid presets are accepted
651        """
652        presets = ["-fastest", "-fast", "-medium",
653                   "-thorough", "-verythorough", "-exhaustive"]
654
655        imIn = self.get_ref_image_path("LDR", "input", "A")
656        imOut = self.get_tmp_image_path("LDR", "decomp")
657
658        for preset in presets:
659            with self.subTest(preset=preset):
660                command = [self.binary, "-tl", imIn, imOut, "4x4", preset]
661                self.exec(command)
662                colIn = tli.Image(imIn).get_colors((0, 0))
663                colOut = tli.Image(imOut).get_colors((0, 0))
664                self.assertColorSame(colIn, colOut)
665
666    def test_valid_ldr_input_formats(self):
667        """
668        Test valid LDR input file formats.
669        """
670        imgFormats = ["bmp", "dds", "jpg", "ktx", "png", "tga"]
671
672        for imgFormat in imgFormats:
673            with self.subTest(imgFormat=imgFormat):
674                imIn = "./Test/Data/Tiles/ldr.%s" % imgFormat
675                imOut = self.get_tmp_image_path("LDR", "decomp")
676
677                command = [self.binary, "-tl", imIn, imOut, "4x4", "-fast"]
678                self.exec(command)
679
680                # Check colors if image wrapper supports it
681                if tli.Image.is_format_supported(imgFormat):
682                    colIn = tli.Image(imIn).get_colors((7, 7))
683                    colOut = tli.Image(imOut).get_colors((7, 7))
684
685                    # Catch exception and add fallback for tga handling
686                    # having unstable origin in ImageMagick
687                    try:
688                        self.assertColorSame(colIn, colOut)
689                        continue
690                    except AssertionError as ex:
691                        if imgFormat != "tga":
692                            raise ex
693
694                    # Try yflipped TGA image
695                    colIn = tli.Image(imIn).get_colors((7, 7))
696                    colOut = tli.Image(imOut).get_colors((7, 1))
697                    self.assertColorSame(colIn, colOut)
698
699    def test_valid_uncomp_ldr_output_formats(self):
700        """
701        Test valid uncompressed LDR output file formats.
702        """
703        imgFormats = ["bmp", "dds", "ktx", "png", "tga"]
704
705        for imgFormat in imgFormats:
706            with self.subTest(imgFormat=imgFormat):
707                imIn = self.get_ref_image_path("LDR", "input", "A")
708                imOut = self.get_tmp_image_path("EXP", ".%s" % imgFormat)
709
710                command = [self.binary, "-tl", imIn, imOut, "4x4", "-fast"]
711                self.exec(command)
712
713                # Check colors if image wrapper supports it
714                if tli.Image.is_format_supported(imgFormat):
715                    colIn = tli.Image(imIn).get_colors((7, 7))
716                    colOut = tli.Image(imOut).get_colors((7, 7))
717                    self.assertColorSame(colIn, colOut)
718
719    def test_valid_comp_ldr_output_formats(self):
720        """
721        Test valid compressed LDR output file formats.
722        """
723        imgFormats = ["astc", "ktx"]
724
725        for imgFormat in imgFormats:
726            with self.subTest(imgFormat=imgFormat):
727                imIn = self.get_ref_image_path("LDR", "input", "A")
728                imOut = self.get_tmp_image_path("EXP", ".%s" % imgFormat)
729                imOut2 = self.get_tmp_image_path("LDR", "decomp")
730
731                command = [self.binary, "-cl", imIn, imOut, "4x4", "-fast"]
732                self.exec(command)
733
734                command = [self.binary, "-dl", imOut, imOut2]
735                self.exec(command)
736
737                # Check colors if image wrapper supports it
738                if tli.Image.is_format_supported(imgFormat):
739                    colIn = tli.Image(imIn).get_colors((7, 7))
740                    colOut = tli.Image(imOut2).get_colors((7, 7))
741                    self.assertColorSame(colIn, colOut2)
742
743    def test_valid_hdr_input_formats(self):
744        """
745        Test valid HDR input file formats.
746        """
747        imgFormats = ["exr", "hdr"]
748
749        for imgFormat in imgFormats:
750            with self.subTest(imgFormat=imgFormat):
751                imIn = "./Test/Data/Tiles/hdr.%s" % imgFormat
752                imOut = self.get_tmp_image_path("HDR", "decomp")
753
754                command = [self.binary, "-th", imIn, imOut, "4x4", "-fast"]
755                self.exec(command)
756
757                # Check colors if image wrapper supports it
758                if tli.Image.is_format_supported(imgFormat, profile="hdr"):
759                    colIn = tli.Image(imIn).get_colors((7, 7))
760                    colOut = tli.Image(imOut).get_colors((7, 7))
761                    self.assertColorSame(colIn, colOut)
762
763    def test_valid_uncomp_hdr_output_formats(self):
764        """
765        Test valid uncompressed HDR output file formats.
766        """
767        imgFormats = ["dds", "exr", "hdr", "ktx"]
768
769        for imgFormat in imgFormats:
770            with self.subTest(imgFormat=imgFormat):
771                imIn = self.get_ref_image_path("HDR", "input", "A")
772                imOut = self.get_tmp_image_path("EXP", ".%s" % imgFormat)
773
774                command = [self.binary, "-th", imIn, imOut, "4x4", "-fast"]
775                self.exec(command)
776
777                # Check colors if image wrapper supports it
778                if tli.Image.is_format_supported(imgFormat, profile="hdr"):
779                    colIn = tli.Image(imIn).get_colors((7, 7))
780                    colOut = tli.Image(imOut).get_colors((7, 7))
781                    self.assertColorSame(colIn, colOut)
782
783    def test_valid_comp_hdr_output_formats(self):
784        """
785        Test valid compressed HDR output file formats.
786        """
787        imgFormats = ["astc", "ktx"]
788
789        for imgFormat in imgFormats:
790            with self.subTest(imgFormat=imgFormat):
791                imIn = self.get_ref_image_path("HDR", "input", "A")
792                imOut = self.get_tmp_image_path("EXP", ".%s" % imgFormat)
793                imOut2 = self.get_tmp_image_path("HDR", "decomp")
794
795                command = [self.binary, "-ch", imIn, imOut, "4x4", "-fast"]
796                self.exec(command)
797
798                command = [self.binary, "-dh", imOut, imOut2]
799                self.exec(command)
800
801                # Check colors if image wrapper supports it
802                if tli.Image.is_format_supported(imgFormat):
803                    colIn = tli.Image(imIn).get_colors((7, 7))
804                    colOut = tli.Image(imOut2).get_colors((7, 7))
805                    self.assertColorSame(colIn, colOut2)
806
807    def test_compress_normal_psnr(self):
808        """
809        Test compression of normal textures using PSNR error metrics.
810        """
811        decompFile = self.get_tmp_image_path("LDR", "decomp")
812
813        command = [
814            self.binary, "-tl",
815            "./Test/Images/Small/LDR-XY/ldr-xy-00.png",
816            decompFile, "5x5", "-exhaustive"]
817
818        refdB = float(self.exec(command, LDR_RGB_PSNR_PATTERN))
819
820        command.append("-normal")
821        testdB = float(self.exec(command, LDR_RGB_PSNR_PATTERN))
822
823        # Note that this test simply asserts that the "-normal_psnr" is
824        # connected and affects the output. We don't test it does something
825        # useful; that it outside the scope of this test case.
826        self.assertNotEqual(refdB, testdB)
827
828    def test_compress_normal_percep(self):
829        """
830        Test compression of normal textures using perceptual error metrics.
831        """
832        decompFile = self.get_tmp_image_path("LDR", "decomp")
833
834        command = [
835            self.binary, "-tl",
836            "./Test/Images/Small/LDR-XY/ldr-xy-00.png",
837            decompFile, "4x4", "-exhaustive"]
838
839        refdB = float(self.exec(command, LDR_RGB_PSNR_PATTERN))
840
841        command.append("-normal")
842        command.append("-perceptual")
843        testdB = float(self.exec(command, LDR_RGB_PSNR_PATTERN))
844
845        # Note that this test simply asserts that the "-normal -percep" is
846        # connected and affects the output. We don't test it does something
847        # useful; that it outside the scope of this test case.
848        self.assertNotEqual(refdB, testdB)
849
850    def test_compress_esw(self):
851        """
852        Test compression swizzles.
853        """
854        # The swizzles to test
855        swizzles = ["rgba", "g0r1", "rrrg"]
856
857        # Compress a swizzled image
858        for swizzle in swizzles:
859            with self.subTest(swizzle=swizzle):
860                decompFile = self.get_tmp_image_path("LDR", "decomp")
861
862                command = [
863                    self.binary, "-tl",
864                    "./Test/Data/Tiles/ldr.png",
865                    decompFile, "4x4", "-exhaustive",
866                    "-esw", swizzle]
867
868                self.exec(command)
869
870                # Fetch the three color
871                img = tli.Image(decompFile)
872                colorVal = img.get_colors([(7, 7)])
873                colorRef = self.get_color_refs("LDR", "BR")
874                self.assertColorSame(colorRef[0], colorVal[0], swiz=swizzle)
875
876    def test_compress_dsw(self):
877        """
878        Test decompression swizzles.
879        """
880        # The swizzles to test
881        swizzles = ["rgba", "g0r1", "rrrg"]
882
883        # Decompress a swizzled image
884        for swizzle in swizzles:
885            with self.subTest(swizzle=swizzle):
886                decompFile = self.get_tmp_image_path("LDR", "decomp")
887
888                command = [
889                    self.binary, "-tl",
890                    "./Test/Data/Tiles/ldr.png",
891                    decompFile, "4x4", "-exhaustive",
892                    "-dsw", swizzle]
893
894                self.exec(command)
895
896                # Fetch the three color
897                img = tli.Image(decompFile)
898                colorVal = img.get_colors([(7, 7)])
899                colorRef = self.get_color_refs("LDR", "BR")
900                self.assertColorSame(colorRef[0], colorVal[0], swiz=swizzle)
901
902    def test_compress_esw_dsw(self):
903        """
904        Test compression and decompression swizzles
905        """
906        # Compress a swizzled image, and swizzle back in decompression
907        decompFile = self.get_tmp_image_path("LDR", "decomp")
908
909        command = [
910            self.binary, "-tl",
911            "./Test/Data/Tiles/ldr.png",
912            decompFile, "4x4", "-exhaustive",
913            "-esw", "gbar", "-dsw", "argb"]
914
915        self.exec(command)
916
917        # Fetch the three color
918        img = tli.Image(decompFile)
919        colorVal = img.get_colors([(7, 7)])
920        colorRef = self.get_color_refs("LDR", "BR")
921        self.assertColorSame(colorRef[0], colorVal[0])
922
923    def test_compress_flip(self):
924        """
925        Test LDR image flip on compression.
926        """
927        # Compress a flipped image
928        compFile = self.get_tmp_image_path("LDR", "comp")
929
930        command = [
931            self.binary, "-cl",
932            "./Test/Data/Tiles/ldr.png",
933            compFile, "4x4", "-fast", "-yflip"]
934
935        self.exec(command)
936
937        # Decompress a non-flipped image
938        decompFile = self.get_tmp_image_path("LDR", "decomp")
939
940        command = [
941            self.binary, "-dl",
942            compFile,
943            decompFile]
944
945        self.exec(command)
946
947        # Compare TL (0, 0) with BL - should match
948        colorRef = self.get_color_refs("LDR", "BL")
949
950        img = tli.Image(decompFile)
951        colorVal = img.get_colors([(0, 0)])
952        self.assertColorSame(colorRef[0], colorVal[0])
953
954    def test_decompress_flip(self):
955        """
956        Test LDR image flip on decompression.
957        """
958        # Compress a non-flipped image
959        compFile = self.get_tmp_image_path("LDR", "comp")
960
961        command = [
962            self.binary, "-cl",
963            "./Test/Data/Tiles/ldr.png",
964            compFile, "4x4", "-fast"]
965
966        self.exec(command)
967
968        # Decompress a flipped image
969        decompFile = self.get_tmp_image_path("LDR", "decomp")
970
971        command = [
972            self.binary, "-dl",
973            compFile,
974            decompFile, "-yflip"]
975
976        self.exec(command)
977
978        # Compare TL (0, 0) with BL - should match
979        colorRef = self.get_color_refs("LDR", "BL")
980
981        img = tli.Image(decompFile)
982        colorVal = img.get_colors([(0, 0)])
983        self.assertColorSame(colorRef[0], colorVal[0])
984
985    def test_roundtrip_flip(self):
986        """
987        Test LDR image flip on roundtrip (no flip should occur).
988        """
989        # Compress and decompressed a flipped LDR image
990        decompFile = self.get_tmp_image_path("LDR", "decomp")
991
992        command = [
993            self.binary, "-tl",
994            "./Test/Data/Tiles/ldr.png",
995            decompFile, "4x4", "-fast", "-yflip"]
996
997        self.exec(command)
998
999        # Compare TL (0, 0) with TL - should match - i.e. no flip
1000        colorRef = self.get_color_refs("LDR", "TL")
1001
1002        img = tli.Image(decompFile)
1003        colorVal = img.get_colors([(0, 0)])
1004
1005        self.assertColorSame(colorRef[0], colorVal[0])
1006
1007    def test_channel_weighting(self):
1008        """
1009        Test channel weighting.
1010        """
1011        inputFile = "./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png"
1012        decompFile = self.get_tmp_image_path("LDR", "decomp")
1013
1014        # Compute the basic image without any channel weights
1015        command = [
1016            self.binary, "-tl",
1017            inputFile, decompFile, "4x4", "-medium"]
1018
1019        self.exec(command)
1020        baseRMSE = self.get_channel_rmse(inputFile, decompFile)
1021
1022        # Note: Using -cw can result in a worse result than not using -cw,
1023        # with regressions in RMSE for the high-weighted channel. This is
1024        # particularly an issue in synthetic images, as they are more likely to
1025        # hit corner cases in the heuristics. It happens to "work" for the
1026        # selected test image and these settings, but might start to fail in
1027        # future due to compressor changes.
1028
1029        # Test each channel with a high weight
1030        for chIdx, chName in ((0, "R"), (1, "G"), (2, "B"), (3, "A")):
1031            with self.subTest(channel=chName):
1032                cwArg = ["%s" % (10 if x == chIdx else 1) for x in range(0, 4)]
1033                command2 = command + ["-cw"] + cwArg
1034                self.exec(command2)
1035                chRMSE = self.get_channel_rmse(inputFile, decompFile)
1036                self.assertLess(chRMSE[chIdx], baseRMSE[chIdx])
1037
1038    def test_partition_count_limit(self):
1039        """
1040        Test partition count limit.
1041        """
1042        inputFile = "./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png"
1043        decompFile = self.get_tmp_image_path("LDR", "decomp")
1044
1045        # Compute the basic image without any channel weights
1046        command = [
1047            self.binary, "-tl",
1048            inputFile, decompFile, "4x4", "-medium"]
1049
1050        self.exec(command)
1051        refRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1052
1053        command += ["-partitioncountlimit", "1"]
1054        self.exec(command)
1055        testRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1056
1057        # RMSE should get worse (higher) if we reduce search space
1058        self.assertGreater(testRMSE, refRMSE)
1059
1060    def test_2partition_index_limit(self):
1061        """
1062        Test partition index limit.
1063        """
1064        inputFile = "./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png"
1065        decompFile = self.get_tmp_image_path("LDR", "decomp")
1066
1067        # Compute the basic image without any channel weights
1068        command = [
1069            self.binary, "-tl",
1070            inputFile, decompFile, "4x4", "-medium"]
1071
1072        self.exec(command)
1073        refRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1074
1075        command += ["-2partitionindexlimit", "1"]
1076        self.exec(command)
1077        testRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1078
1079        # RMSE should get worse (higher) if we reduce search space
1080        self.assertGreater(testRMSE, refRMSE)
1081
1082    def test_3partition_index_limit(self):
1083        """
1084        Test partition index limit.
1085        """
1086        inputFile = "./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png"
1087        decompFile = self.get_tmp_image_path("LDR", "decomp")
1088
1089        # Compute the basic image without any channel weights
1090        command = [
1091            self.binary, "-tl",
1092            inputFile, decompFile, "4x4", "-medium"]
1093
1094        self.exec(command)
1095        refRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1096
1097        command += ["-3partitionindexlimit", "1"]
1098        self.exec(command)
1099        testRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1100
1101        # RMSE should get worse (higher) if we reduce search space
1102        self.assertGreater(testRMSE, refRMSE)
1103
1104    def test_4partition_index_limit(self):
1105        """
1106        Test partition index limit.
1107        """
1108        inputFile = "./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png"
1109        decompFile = self.get_tmp_image_path("LDR", "decomp")
1110
1111        # Compute the basic image without any channel weights
1112        command = [
1113            self.binary, "-tl",
1114            inputFile, decompFile, "4x4", "-medium"]
1115
1116        self.exec(command)
1117        refRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1118
1119        command += ["-4partitionindexlimit", "1"]
1120        self.exec(command)
1121        testRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1122
1123        # RMSE should get worse (higher) if we reduce search space
1124        self.assertGreater(testRMSE, refRMSE)
1125
1126    def test_blockmode_limit(self):
1127        """
1128        Test block mode limit.
1129        """
1130        inputFile = "./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png"
1131        decompFile = self.get_tmp_image_path("LDR", "decomp")
1132
1133        # Compute the basic image without any channel weights
1134        command = [
1135            self.binary, "-tl",
1136            inputFile, decompFile, "4x4", "-medium"]
1137
1138        self.exec(command)
1139        refRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1140
1141        command += ["-blockmodelimit", "25"]
1142        self.exec(command)
1143        testRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1144
1145        # RMSE should get worse (higher) if we reduce search space
1146        self.assertGreater(testRMSE, refRMSE)
1147
1148    def test_refinement_limit(self):
1149        """
1150        Test refinement limit.
1151        """
1152        inputFile = "./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png"
1153        decompFile = self.get_tmp_image_path("LDR", "decomp")
1154
1155        command = [
1156            self.binary, "-tl",
1157            inputFile, decompFile, "4x4", "-medium"]
1158
1159        self.exec(command)
1160        refRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1161
1162        command += ["-refinementlimit", "1"]
1163        self.exec(command)
1164        testRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1165
1166        # RMSE should get worse (higher) if we reduce search space
1167        self.assertGreater(testRMSE, refRMSE)
1168
1169    def test_candidate_limit(self):
1170        """
1171        Test candidate limit.
1172        """
1173        inputFile = "./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png"
1174        decompFile = self.get_tmp_image_path("LDR", "decomp")
1175
1176        command = [
1177            self.binary, "-tl",
1178            inputFile, decompFile, "4x4", "-medium"]
1179
1180        self.exec(command)
1181        refRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1182
1183        command += ["-candidatelimit", "1"]
1184        self.exec(command)
1185        testRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1186
1187        # RMSE should get worse (higher) if we reduce search space
1188        self.assertGreater(testRMSE, refRMSE)
1189
1190    def test_db_cutoff_limit(self):
1191        """
1192        Test db cutoff limit.
1193        """
1194        inputFile = "./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png"
1195        decompFile = self.get_tmp_image_path("LDR", "decomp")
1196
1197        # Compute the basic image without any channel weights
1198        command = [
1199            self.binary, "-tl",
1200            inputFile, decompFile, "4x4", "-medium"]
1201
1202        self.exec(command)
1203        refRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1204
1205        command += ["-dblimit", "10"]
1206        self.exec(command)
1207        testRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1208
1209        # RMSE should get worse (higher) if we reduce cutoff quality
1210        self.assertGreater(testRMSE, refRMSE)
1211
1212    def test_2partition_early_limit(self):
1213        """
1214        Test 2 partition early limit.
1215        """
1216        inputFile = "./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png"
1217        decompFile = self.get_tmp_image_path("LDR", "decomp")
1218
1219        # Compute the basic image without any channel weights
1220        command = [
1221            self.binary, "-tl",
1222            inputFile, decompFile, "4x4", "-medium"]
1223
1224        self.exec(command)
1225        refRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1226
1227        command += ["-2partitionlimitfactor", "1.0"]
1228        self.exec(command)
1229        testRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1230
1231        # RMSE should get worse (higher) if we reduce search space
1232        self.assertGreater(testRMSE, refRMSE)
1233
1234    def test_3partition_early_limit(self):
1235        """
1236        Test 3 partition early limit.
1237        """
1238        inputFile = "./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png"
1239        decompFile = self.get_tmp_image_path("LDR", "decomp")
1240
1241        # Compute the basic image without any channel weights
1242        command = [
1243            self.binary, "-tl",
1244            inputFile, decompFile, "4x4", "-medium"]
1245
1246        self.exec(command)
1247        refRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1248
1249        command += ["-3partitionlimitfactor", "1.0"]
1250        self.exec(command)
1251        testRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1252
1253        # RMSE should get worse (higher) if we reduce search space
1254        self.assertNotEqual(testRMSE, refRMSE)
1255
1256    def test_2plane_correlation_limit(self):
1257        """
1258        Test 2 plane correlation limit.
1259        """
1260        inputFile = "./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png"
1261        decompFile = self.get_tmp_image_path("LDR", "decomp")
1262
1263        # Compute the basic image without any channel weights
1264        command = [
1265            self.binary, "-tl",
1266            inputFile, decompFile, "4x4", "-medium"]
1267
1268        self.exec(command)
1269        refRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1270
1271        command += ["-2planelimitcorrelation", "0.1"]
1272        self.exec(command)
1273        testRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1274
1275        # RMSE should get worse (higher) if we reduce search space
1276        self.assertGreater(testRMSE, refRMSE)
1277
1278    def test_2partition_candidate_limit(self):
1279        """
1280        Test 2 partition partitioning candidate limit.
1281        """
1282        inputFile = "./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png"
1283        decompFile = self.get_tmp_image_path("LDR", "decomp")
1284
1285        # Compute the basic image without any channel weights
1286        command = [
1287            self.binary, "-tl",
1288            inputFile, decompFile, "4x4", "-medium"]
1289
1290        self.exec(command)
1291        refRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1292
1293        command += ["-2partitioncandidatelimit", "1"]
1294        self.exec(command)
1295        testRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1296
1297        # RMSE should get worse (higher) if we reduce search space
1298        self.assertGreater(testRMSE, refRMSE)
1299
1300    def test_3partition_candidate_limit(self):
1301        """
1302        Test 3 partition partitioning candidate limit.
1303        """
1304        inputFile = "./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png"
1305        decompFile = self.get_tmp_image_path("LDR", "decomp")
1306
1307        # Compute the basic image without any channel weights
1308        command = [
1309            self.binary, "-tl",
1310            inputFile, decompFile, "4x4", "-medium"]
1311
1312        self.exec(command)
1313        refRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1314
1315        command += ["-3partitioncandidatelimit", "1"]
1316        self.exec(command)
1317        testRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1318
1319        # RMSE should get worse (higher) if we reduce search space
1320        self.assertGreater(testRMSE, refRMSE)
1321
1322    def test_4partition_candidate_limit(self):
1323        """
1324        Test 4 partition partitioning candidate limit.
1325        """
1326        inputFile = "./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png"
1327        decompFile = self.get_tmp_image_path("LDR", "decomp")
1328
1329        # Compute the basic image without any channel weights
1330        command = [
1331            self.binary, "-tl",
1332            inputFile, decompFile, "4x4", "-medium"]
1333
1334        self.exec(command)
1335        refRMSE = sum(self.get_channel_rmse(inputFile, decompFile))
1336
1337        command += ["-4partitioncandidatelimit", "1"]
1338        self.exec(command)
1339
1340        # RMSE should get worse (higher) if we reduce search space
1341        # Don't check this here, as 4 partitions not used in any Small image
1342        # even for -exhaustive, BUT command line option must be accepted and
1343        # not error ...
1344        # self.assertGreater(testRMSE, refRMSE)
1345
1346    @unittest.skipIf(os.cpu_count() == 1, "Cannot test on single core host")
1347    def test_thread_count(self):
1348        """
1349        Test codec thread count.
1350        """
1351        inputFile = "./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png"
1352        decompFile = self.get_tmp_image_path("LDR", "decomp")
1353
1354        # Compute the basic image without any channel weights
1355        command = [
1356            self.binary, "-tl",
1357            inputFile, decompFile, "4x4", "-medium"]
1358
1359        start = time.time()
1360        self.exec(command)
1361        refTime = time.time() - start
1362
1363        command += ["-j", "1"]
1364        start = time.time()
1365        self.exec(command)
1366        testTime = time.time() - start
1367
1368        # Test time should get slower with fewer threads
1369        self.assertGreater(testTime, refTime)
1370
1371    def test_silent(self):
1372        """
1373        Test silent
1374        """
1375        inputFile = "./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png"
1376        decompFile = self.get_tmp_image_path("LDR", "decomp")
1377
1378        # Compute the basic image without any channel weights
1379        command = [
1380            self.binary, "-tl",
1381            inputFile, decompFile, "4x4", "-medium"]
1382        stdout = self.exec(command)
1383
1384        command += ["-silent"]
1385        stdoutSilent = self.exec(command)
1386
1387        # Check that stdout is shorter in silent mode. Note that this doesn't
1388        # check that it is as silent as it should be, just that silent is wired
1389        # somewhere ...
1390        self.assertLess(len(stdoutSilent), len(stdout))
1391
1392    def test_image_quality_stability(self):
1393        """
1394        Test that a round-trip and a file-based round-trip give same result.
1395        """
1396        inputFile = "./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png"
1397        p1DecFile = self.get_tmp_image_path("LDR", "decomp")
1398        p2CompFile = self.get_tmp_image_path("LDR", "comp")
1399        p2DecFile = self.get_tmp_image_path("LDR", "decomp")
1400
1401        # Compute the first image using a direct round-trip
1402        command = [self.binary, "-tl", inputFile, p1DecFile, "4x4", "-medium"]
1403        self.exec(command)
1404
1405        # Compute the first image using a file-based round-trip
1406        command = [self.binary, "-cl", inputFile, p2CompFile, "4x4", "-medium",
1407                   "-decode_unorm8"]
1408        self.exec(command)
1409        command = [self.binary, "-dl", p2CompFile, p2DecFile]
1410        self.exec(command)
1411
1412        # RMSE should be the same
1413        p1RMSE = sum(self.get_channel_rmse(inputFile, p1DecFile))
1414        p2RMSE = sum(self.get_channel_rmse(inputFile, p2DecFile))
1415        self.assertEqual(p1RMSE, p2RMSE)
1416
1417
1418class CLINTest(CLITestBase):
1419    """
1420    Command line interface negative tests.
1421
1422    These tests are designed to test that bad inputs to the command line are
1423    handled cleanly and that errors are correctly thrown.
1424
1425    Note that many tests are mutations of a valid positive test command line,
1426    to ensure that the base command line is valid before it is mutated many
1427    of these tests include a *positive test* to ensure that the starting point
1428    is actually a valid command line (otherwise we could be throwing an
1429    arbitrary error).
1430    """
1431
1432    def exec(self, command, expectPass=False):
1433        """
1434        Execute a negative test.
1435
1436        Test will automatically fail if:
1437
1438        * The subprocess return code is zero, unless ``expectPass==True``.
1439        * The subprocess correctly returned non-zero, but without any error
1440          message.
1441        * The subprocess dies with any kind of signal.
1442
1443        Args:
1444            command (list(str)): The command to execute.
1445            expectPass (bool): ``True`` if this command is actually expected to
1446                pass, which is used to validate commands before mutating them.
1447        """
1448        try:
1449            result = sp.run(command, stdout=sp.PIPE, stderr=sp.PIPE,
1450                            universal_newlines=True, check=True)
1451            error = False
1452        except sp.CalledProcessError as ex:
1453            # Pop out of the CPE scope to handle the error, as this reduces
1454            # test log verbosity on failure by avoiding nested exceptions
1455            result = ex
1456            error = True
1457
1458        rcode = result.returncode
1459
1460        # Emit debug logging if needed (negative rcode is a signal)
1461        badResult = (error == expectPass) or (rcode < 0)
1462
1463        if ASTCENC_CLI_ALWAYS or (badResult and ASTCENC_CLI_ON_ERROR_NEG):
1464            # Format for shell replay
1465            print("\n" + " ".join(command))
1466            # Format for script command list replay
1467            print("\n" + ", ".join(("\"%s\"" % x for x in command)))
1468
1469        if ASTCENC_LOG_ALWAYS or (badResult and ASTCENC_LOG_ON_ERROR_NEG):
1470            print(result.stdout)
1471
1472        # If we expected a pass, then rcode == 0
1473        if expectPass:
1474            self.assertEqual(rcode, 0, "Exec did not pass as expected")
1475            self.assertNotIn("ERROR", result.stderr)
1476            return
1477
1478        # If we got a negative that's always bad (signal of some kind)
1479        if rcode < 0:
1480            msg = "Exec died with signal %s" % signal.Signals(-rcode).name
1481            self.assertGreaterEqual(rcode, 0, msg)
1482
1483        # Otherwise just assert that we got an error log, and some positive
1484        # return code value was returned
1485        self.assertIn("ERROR", result.stderr)
1486        self.assertGreater(rcode, 0, "Exec did not fail as expected")
1487
1488    def exec_with_omit(self, command, startOmit):
1489        """
1490        Execute a negative test with command line argument omission.
1491
1492        These tests aim to prove that the command fails if arguments are
1493        missing. However the passed command MUST be a valid command which
1494        passes if no argument are omitted (this is checked, to ensure that
1495        the test case is a valid test).
1496
1497        Test will automatically fail if:
1498
1499        * A partial command doesn't fail.
1500        * The full command doesn't pass.
1501        """
1502        # Run the command, incrementally omitting arguments
1503        commandLen = len(command)
1504        for subLen in range(startOmit, commandLen + 1):
1505            omit = len(command) - subLen
1506            with self.subTest(omit=omit):
1507                testCommand = command[:subLen]
1508                expectPass = omit == 0
1509                self.exec(testCommand, expectPass)
1510
1511    def test_cl_missing_args(self):
1512        """
1513        Test -cl with missing arguments.
1514        """
1515        # Build a valid command
1516        command = [
1517            self.binary, "-cl",
1518            self.get_ref_image_path("LDR", "input", "A"),
1519            self.get_tmp_image_path("LDR", "comp"),
1520            "4x4", "-fast"]
1521
1522        self.exec_with_omit(command, 2)
1523
1524    def test_cl_missing_input(self):
1525        """
1526        Test -cl with a missing input file.
1527        """
1528        # Build a valid command with a missing input file
1529        command = [
1530            self.binary, "-cl",
1531            "./Test/Data/missing.png",
1532            self.get_tmp_image_path("LDR", "comp"),
1533            "4x4", "-fast"]
1534
1535        self.exec(command)
1536
1537    def test_cl_missing_input_array_slice(self):
1538        """
1539        Test -cl with a missing input file in an array slice.
1540        """
1541        # Build a valid command with a missing input file
1542        command = [
1543            self.binary, "-cl",
1544            "./Test/Data/Tiles/ldr.png",
1545            self.get_tmp_image_path("LDR", "comp"),
1546            "3x3x3", "-fast", "-zdim", "3"]
1547
1548        self.exec(command)
1549
1550    def test_cl_unknown_input(self):
1551        """
1552        Test -cl with an unknown input file extension.
1553        """
1554        # Build an otherwise valid command with the test flaw
1555        command = [
1556            self.binary, "-cl",
1557            "./Test/Data/empty.unk",
1558            self.get_tmp_image_path("LDR", "comp"),
1559            "4x4", "-fast"]
1560
1561        self.exec(command)
1562
1563    def test_cl_missing_output(self):
1564        """
1565        Test -cl with a missing output directory.
1566        """
1567        # Build an otherwise valid command with the test flaw
1568        command = [
1569            self.binary, "-cl",
1570            self.get_ref_image_path("LDR", "input", "A"),
1571            "./DoesNotExist/test.astc",
1572            "4x4", "-fast"]
1573
1574        self.exec(command)
1575
1576    def test_cl_unknown_output(self):
1577        """
1578        Test -cl with an unknown output file extension.
1579        """
1580        # Build an otherwise valid command with the test flaw
1581        command = [
1582            self.binary, "-cl",
1583            self.get_ref_image_path("LDR", "input", "A"),
1584            "./test.aastc",
1585            "4x4", "-fast"]
1586
1587        self.exec(command)
1588
1589    def test_cl_bad_block_size(self):
1590        """
1591        Test -cl with an invalid block size.
1592        """
1593        badSizes = [
1594            "4x5",      # Illegal 2D block size
1595            "3x3x4",    # Illegal 3D block size
1596            "4x4x4x4",  # Too many dimensions
1597            "4x",       # Incomplete 2D block size
1598            "4x4x",     # Incomplete 3D block size
1599            "4x4x4x",   # Over-long 3D block size
1600            "4xe",      # Illegal non-numeric character
1601            "4x4e"      # Additional non-numeric character
1602        ]
1603
1604        # Build an otherwise valid command with the test flaw
1605        command = [
1606            self.binary, "-cl",
1607            self.get_ref_image_path("LDR", "input", "A"),
1608            self.get_tmp_image_path("LDR", "comp"),
1609            "4x4", "-fast"]
1610
1611        # Test that the underlying command is valid
1612        self.exec(command, True)
1613
1614        blockIndex = command.index("4x4")
1615        for badSize in badSizes:
1616            with self.subTest(blockSize=badSize):
1617                command[blockIndex] = badSize
1618                self.exec(command)
1619
1620    def test_cl_bad_preset(self):
1621        """
1622        Test -cl with an invalid encoding preset.
1623        """
1624        # Build an otherwise valid command with the test flaw
1625        command = [
1626            self.binary, "-cl",
1627            self.get_ref_image_path("LDR", "input", "A"),
1628            self.get_tmp_image_path("LDR", "comp"),
1629            "4x4", "-fastt"]
1630
1631        self.exec(command)
1632
1633    def test_cl_bad_argument(self):
1634        """
1635        Test -cl with an unknown additional argument.
1636        """
1637        # Build an otherwise valid command with the test flaw
1638        command = [
1639            self.binary, "-cl",
1640            self.get_ref_image_path("LDR", "input", "A"),
1641            self.get_tmp_image_path("LDR", "comp"),
1642            "4x4", "-fast", "-unknown"]
1643
1644        self.exec(command)
1645
1646    def test_cl_2d_block_with_array(self):
1647        """
1648        Test -cl with a 2D block size and 3D input data.
1649        """
1650        # Build an otherwise valid command with the test flaw
1651
1652        # TODO: This fails late (i.e. the data is still loaded, and we fail
1653        # at processing time when we see a 3D array). We could fail earlier at
1654        # parse time, which might consolidate the error handling code.
1655        command = [
1656            self.binary, "-cl",
1657            "./Test/Data/Tiles/ldr.png",
1658            self.get_tmp_image_path("LDR", "comp"),
1659            "4x4", "-fast", "-zdim", "2"]
1660
1661        self.exec(command)
1662
1663    def test_cl_array_missing_args(self):
1664        """
1665        Test -cl with a 2D block size and 3D input data.
1666        """
1667        # Build an otherwise valid command
1668        command = [
1669            self.binary, "-cl",
1670            "./Test/Data/Tiles/ldr.png",
1671            self.get_tmp_image_path("LDR", "comp"),
1672            "4x4x4", "-fast", "-zdim", "2"]
1673
1674        # Run the command, incrementally omitting arguments
1675        self.exec_with_omit(command, 7)
1676
1677    def test_tl_missing_args(self):
1678        """
1679        Test -tl with missing arguments.
1680        """
1681        # Build a valid command
1682        command = [
1683            self.binary, "-tl",
1684            self.get_ref_image_path("LDR", "input", "A"),
1685            self.get_tmp_image_path("LDR", "decomp"),
1686            "4x4", "-fast"]
1687
1688        # Run the command, incrementally omitting arguments
1689        self.exec_with_omit(command, 2)
1690
1691    def test_tl_missing_input(self):
1692        """
1693        Test -tl with a missing input file.
1694        """
1695        # Build a valid command with a missing input file
1696        command = [
1697            self.binary, "-tl",
1698            "./Test/Data/missing.png",
1699            self.get_tmp_image_path("LDR", "decomp"),
1700            "4x4", "-fast"]
1701
1702        self.exec(command)
1703
1704    def test_tl_unknown_input(self):
1705        """
1706        Test -tl with an unknown input file extension.
1707        """
1708        # Build an otherwise valid command with the test flaw
1709        command = [
1710            self.binary, "-tl",
1711            "./Test/Data/empty.unk",
1712            self.get_tmp_image_path("LDR", "decomp"),
1713            "4x4", "-fast"]
1714
1715        self.exec(command)
1716
1717    def test_tl_missing_output(self):
1718        """
1719        Test -tl with a missing output directory.
1720        """
1721        # Build an otherwise valid command with the test flaw
1722        command = [
1723            self.binary, "-tl",
1724            self.get_ref_image_path("LDR", "input", "A"),
1725            "./DoesNotExist/test.png",
1726            "4x4", "-fast"]
1727
1728        self.exec(command)
1729
1730    def test_tl_bad_block_size(self):
1731        """
1732        Test -tl with an invalid block size.
1733        """
1734        badSizes = [
1735            "4x5",      # Illegal 2D block size
1736            "3x3x4",    # Illegal 3D block size
1737            "4x4x4x4",  # Too many dimensions
1738            "4x",       # Incomplete 2D block size
1739            "4x4x",     # Incomplete 3D block size
1740            "4x4x4x",   # Over-long 3D block size
1741            "4xe",      # Illegal non-numeric character
1742            "4x4e"      # Additional non-numeric character
1743        ]
1744
1745        # Build an otherwise valid command with the test flaw
1746        command = [
1747            self.binary, "-tl",
1748            self.get_ref_image_path("LDR", "input", "A"),
1749            self.get_tmp_image_path("LDR", "decomp"),
1750            "4x4", "-fast"]
1751
1752        # Test that the underlying command is valid
1753        self.exec(command, True)
1754
1755        blockIndex = command.index("4x4")
1756        for badSize in badSizes:
1757            with self.subTest(blockSize=badSize):
1758                command[blockIndex] = badSize
1759                self.exec(command)
1760
1761    def test_tl_bad_preset(self):
1762        """
1763        Test -tl with an invalid encoding preset.
1764        """
1765        # Build an otherwise valid command with the test flaw
1766        command = [
1767            self.binary, "-tl",
1768            self.get_ref_image_path("LDR", "input", "A"),
1769            self.get_tmp_image_path("LDR", "decomp"),
1770            "4x4", "-fastt"]
1771
1772        self.exec(command)
1773
1774    def test_tl_bad_argument(self):
1775        """
1776        Test -tl with an unknown additional argument.
1777        """
1778        # Build an otherwise valid command with the test flaw
1779        command = [
1780            self.binary, "-tl",
1781            self.get_ref_image_path("LDR", "input", "A"),
1782            self.get_tmp_image_path("LDR", "decomp"),
1783            "4x4", "-fast", "-unknown"]
1784
1785        self.exec(command)
1786
1787    def test_dl_missing_args(self):
1788        """
1789        Test -dl with missing arguments.
1790        """
1791        # Build a valid command
1792        command = [
1793            self.binary, "-dl",
1794            self.get_ref_image_path("LDR", "comp", "A"),
1795            self.get_tmp_image_path("LDR", "decomp")]
1796
1797        # Run the command, incrementally omitting arguments
1798        self.exec_with_omit(command, 2)
1799
1800    def test_dl_missing_output(self):
1801        """
1802        Test -dl with a missing output directory.
1803        """
1804        # Build an otherwise valid command with the test flaw
1805        command = [
1806            self.binary, "-dl",
1807            self.get_ref_image_path("LDR", "comp", "A"),
1808            "./DoesNotExist/test.png"]
1809
1810        self.exec(command)
1811
1812    def test_cl_a_missing_args(self):
1813        """
1814        Test -cl with -a and missing arguments.
1815        """
1816        # Build a valid command
1817        command = [
1818            self.binary, "-cl",
1819            self.get_ref_image_path("LDR", "input", "A"),
1820            self.get_tmp_image_path("LDR", "comp"),
1821            "4x4", "-fast",
1822            "-a", "2"]
1823
1824        # Run the command, incrementally omitting arguments
1825        self.exec_with_omit(command, 7)
1826
1827    def test_cl_cw_missing_args(self):
1828        """
1829        Test -cl with -cw and missing arguments.
1830        """
1831        # Build a valid command
1832        command = [
1833            self.binary, "-cl",
1834            self.get_ref_image_path("LDR", "input", "A"),
1835            self.get_tmp_image_path("LDR", "comp"),
1836            "4x4", "-fast",
1837            "-cw", "0", "1", "2", "3"]
1838
1839        # Run the command, incrementally omitting arguments
1840        self.exec_with_omit(command, 7)
1841
1842    def test_cl_2partitionindexlimit_missing_args(self):
1843        """
1844        Test -cl with -2partitionindexlimit and missing arguments.
1845        """
1846        # Build a valid command
1847        command = [
1848            self.binary, "-cl",
1849            self.get_ref_image_path("LDR", "input", "A"),
1850            self.get_tmp_image_path("LDR", "comp"),
1851            "4x4", "-fast",
1852            "-2partitionindexlimit", "3"]
1853
1854        # Run the command, incrementally omitting arguments
1855        self.exec_with_omit(command, 7)
1856
1857    def test_cl_3partitionindexlimit_missing_args(self):
1858        """
1859        Test -cl with -3partitionindexlimit and missing arguments.
1860        """
1861        # Build a valid command
1862        command = [
1863            self.binary, "-cl",
1864            self.get_ref_image_path("LDR", "input", "A"),
1865            self.get_tmp_image_path("LDR", "comp"),
1866            "4x4", "-fast",
1867            "-3partitionindexlimit", "3"]
1868
1869        # Run the command, incrementally omitting arguments
1870        self.exec_with_omit(command, 7)
1871
1872    def test_cl_4partitionindexlimit_missing_args(self):
1873        """
1874        Test -cl with -4partitionindexlimit and missing arguments.
1875        """
1876        # Build a valid command
1877        command = [
1878            self.binary, "-cl",
1879            self.get_ref_image_path("LDR", "input", "A"),
1880            self.get_tmp_image_path("LDR", "comp"),
1881            "4x4", "-fast",
1882            "-4partitionindexlimit", "3"]
1883
1884        # Run the command, incrementally omitting arguments
1885        self.exec_with_omit(command, 7)
1886
1887    def test_cl_2partitioncandidatelimit_missing_args(self):
1888        """
1889        Test -cl with -2partitioncandidatelimit and missing arguments.
1890        """
1891        # Build a valid command
1892        command = [
1893            self.binary, "-cl",
1894            self.get_ref_image_path("LDR", "input", "A"),
1895            self.get_tmp_image_path("LDR", "comp"),
1896            "4x4", "-fast",
1897            "-2partitioncandidatelimit", "1"]
1898
1899        # Run the command, incrementally omitting arguments
1900        self.exec_with_omit(command, 7)
1901
1902    def test_cl_3partitioncandidatelimit_missing_args(self):
1903        """
1904        Test -cl with -3partitioncandidatelimit and missing arguments.
1905        """
1906        # Build a valid command
1907        command = [
1908            self.binary, "-cl",
1909            self.get_ref_image_path("LDR", "input", "A"),
1910            self.get_tmp_image_path("LDR", "comp"),
1911            "4x4", "-fast",
1912            "-3partitioncandidatelimit", "3"]
1913
1914        # Run the command, incrementally omitting arguments
1915        self.exec_with_omit(command, 7)
1916
1917
1918    def test_cl_4partitioncandidatelimit_missing_args(self):
1919        """
1920        Test -cl with -4partitioncandidatelimit and missing arguments.
1921        """
1922        # Build a valid command
1923        command = [
1924            self.binary, "-cl",
1925            self.get_ref_image_path("LDR", "input", "A"),
1926            self.get_tmp_image_path("LDR", "comp"),
1927            "4x4", "-fast",
1928            "-4partitioncandidatelimit", "3"]
1929
1930        # Run the command, incrementally omitting arguments
1931        self.exec_with_omit(command, 7)
1932
1933    def test_cl_blockmodelimit_missing_args(self):
1934        """
1935        Test -cl with -blockmodelimit and missing arguments.
1936        """
1937        # Build a valid command
1938        command = [
1939            self.binary, "-cl",
1940            self.get_ref_image_path("LDR", "input", "A"),
1941            self.get_tmp_image_path("LDR", "comp"),
1942            "4x4", "-fast",
1943            "-blockmodelimit", "3"]
1944
1945        # Run the command, incrementally omitting arguments
1946        self.exec_with_omit(command, 7)
1947
1948    def test_cl_refinementlimit_missing_args(self):
1949        """
1950        Test -cl with -refinementlimit and missing arguments.
1951        """
1952        # Build a valid command
1953        command = [
1954            self.binary, "-cl",
1955            self.get_ref_image_path("LDR", "input", "A"),
1956            self.get_tmp_image_path("LDR", "comp"),
1957            "4x4", "-fast",
1958            "-refinementlimit", "3"]
1959
1960        # Run the command, incrementally omitting arguments
1961        self.exec_with_omit(command, 7)
1962
1963    def test_cl_dblimit_missing_args(self):
1964        """
1965        Test -cl with -dblimit and missing arguments.
1966        """
1967        # Build a valid command
1968        command = [
1969            self.binary, "-cl",
1970            self.get_ref_image_path("LDR", "input", "A"),
1971            self.get_tmp_image_path("LDR", "comp"),
1972            "4x4", "-fast",
1973            "-dblimit", "3"]
1974
1975        # Run the command, incrementally omitting arguments
1976        self.exec_with_omit(command, 7)
1977
1978    def test_cl_2partitionearlylimit_missing_args(self):
1979        """
1980        Test -cl with -2partitionlimitfactor and missing arguments.
1981        """
1982        # Build a valid command
1983        command = [
1984            self.binary, "-cl",
1985            self.get_ref_image_path("LDR", "input", "A"),
1986            self.get_tmp_image_path("LDR", "comp"),
1987            "4x4", "-fast",
1988            "-2partitionlimitfactor", "3"]
1989
1990        # Run the command, incrementally omitting arguments
1991        self.exec_with_omit(command, 7)
1992
1993    def test_cl_3partitionearlylimit_missing_args(self):
1994        """
1995        Test -cl with -3partitionlimitfactor and missing arguments.
1996        """
1997        # Build a valid command
1998        command = [
1999            self.binary, "-cl",
2000            self.get_ref_image_path("LDR", "input", "A"),
2001            self.get_tmp_image_path("LDR", "comp"),
2002            "4x4", "-fast",
2003            "-3partitionlimitfactor", "3"]
2004
2005        # Run the command, incrementally omitting arguments
2006        self.exec_with_omit(command, 7)
2007
2008    def test_cl_2planeearlylimit_missing_args(self):
2009        """
2010        Test -cl with -2planelimitcorrelation and missing arguments.
2011        """
2012        # Build a valid command
2013        command = [
2014            self.binary, "-cl",
2015            self.get_ref_image_path("LDR", "input", "A"),
2016            self.get_tmp_image_path("LDR", "comp"),
2017            "4x4", "-fast",
2018            "-2planelimitcorrelation", "0.66"]
2019
2020        # Run the command, incrementally omitting arguments
2021        self.exec_with_omit(command, 7)
2022
2023    def test_cl_esw_missing_args(self):
2024        """
2025        Test -cl with -esw and missing arguments.
2026        """
2027        # Build a valid command
2028        command = [
2029            self.binary, "-cl",
2030            self.get_ref_image_path("LDR", "input", "A"),
2031            self.get_tmp_image_path("LDR", "comp"),
2032            "4x4", "-fast",
2033            "-esw", "rgb1"]
2034
2035        # Run the command, incrementally omitting arguments
2036        self.exec_with_omit(command, 7)
2037
2038    def test_cl_esw_invalid_swizzle(self):
2039        """
2040        Test -cl with -esw and invalid swizzles.
2041        """
2042        badSwizzles = [
2043            "",  # Short swizzles
2044            "r",
2045            "rr",
2046            "rrr",
2047            "rrrrr",  # Long swizzles
2048        ]
2049
2050        # Create swizzles with all invalid printable ascii codes
2051        good = ["r", "g", "b", "a", "0", "1"]
2052        for channel in string.printable:
2053            if channel not in good:
2054                badSwizzles.append(channel * 4)
2055
2056        # Build a valid base command
2057        command = [
2058            self.binary, "-cl",
2059            self.get_ref_image_path("LDR", "input", "A"),
2060            self.get_tmp_image_path("LDR", "comp"),
2061            "4x4", "-fast",
2062            "-esw", "rgba"]
2063
2064        blockIndex = command.index("rgba")
2065        for badSwizzle in badSwizzles:
2066            with self.subTest(swizzle=badSwizzle):
2067                command[blockIndex] = badSwizzle
2068                self.exec(command)
2069
2070    def test_cl_ssw_missing_args(self):
2071        """
2072        Test -cl with -ssw and missing arguments.
2073        """
2074        # Build a valid command
2075        command = [
2076            self.binary, "-cl",
2077            self.get_ref_image_path("LDR", "input", "A"),
2078            self.get_tmp_image_path("LDR", "comp"),
2079            "4x4", "-fast",
2080            "-ssw", "rgba"]
2081
2082        # Run the command, incrementally omitting arguments
2083        self.exec_with_omit(command, 7)
2084
2085    def test_cl_ssw_invalid_swizzle(self):
2086        """
2087        Test -cl with -ssw and invalid swizzles.
2088        """
2089        badSwizzles = [
2090            "",  # Short swizzles
2091            "rrrrr",  # Long swizzles
2092        ]
2093
2094        # Create swizzles with all invalid printable ascii codes
2095        good = ["r", "g", "b", "a"]
2096        for channel in string.printable:
2097            if channel not in good:
2098                badSwizzles.append(channel * 4)
2099
2100        # Build a valid base command
2101        command = [
2102            self.binary, "-cl",
2103            self.get_ref_image_path("LDR", "input", "A"),
2104            self.get_tmp_image_path("LDR", "comp"),
2105            "4x4", "-fast",
2106            "-ssw", "rgba"]
2107
2108        blockIndex = command.index("rgba")
2109        for badSwizzle in badSwizzles:
2110            with self.subTest(swizzle=badSwizzle):
2111                command[blockIndex] = badSwizzle
2112                self.exec(command)
2113
2114    def test_dl_dsw_missing_args(self):
2115        """
2116        Test -dl with -dsw and missing arguments.
2117        """
2118        # Build a valid command
2119        command = [
2120            self.binary, "-dl",
2121            self.get_ref_image_path("LDR", "comp", "A"),
2122            self.get_tmp_image_path("LDR", "decomp"),
2123            "-dsw", "rgb1"]
2124
2125        # Run the command, incrementally omitting arguments
2126        self.exec_with_omit(command, 5)
2127
2128    def test_dl_dsw_invalid_swizzle(self):
2129        """
2130        Test -dl with -dsw and invalid swizzles.
2131        """
2132        badSwizzles = [
2133            "",  # Short swizzles
2134            "r",
2135            "rr",
2136            "rrr",
2137            "rrrrr",  # Long swizzles
2138        ]
2139
2140        # Create swizzles with all invalid printable ascii codes
2141        good = ["r", "g", "b", "a", "z", "0", "1"]
2142        for channel in string.printable:
2143            if channel not in good:
2144                badSwizzles.append(channel * 4)
2145
2146        # Build a valid base command
2147        command = [
2148            self.binary, "-dl",
2149            self.get_ref_image_path("LDR", "comp", "A"),
2150            self.get_tmp_image_path("LDR", "decomp"),
2151            "-dsw", "rgba"]
2152
2153        blockIndex = command.index("rgba")
2154        for badSwizzle in badSwizzles:
2155            with self.subTest(swizzle=badSwizzle):
2156                command[blockIndex] = badSwizzle
2157                self.exec(command)
2158
2159    def test_ch_mpsnr_missing_args(self):
2160        """
2161        Test -ch with -mpsnr and missing arguments.
2162        """
2163        # Build a valid command
2164        command = [
2165            self.binary, "-ch",
2166            self.get_ref_image_path("HDR", "input", "A"),
2167            self.get_tmp_image_path("HDR", "comp"),
2168            "4x4", "-fast",
2169            "-mpsnr", "-5", "5"]
2170
2171        # Run the command, incrementally omitting arguments
2172        self.exec_with_omit(command, 7)
2173
2174
2175def main():
2176    """
2177    The main function.
2178
2179    Returns:
2180        int: The process return code.
2181    """
2182    global g_TestEncoder
2183
2184    parser = argparse.ArgumentParser()
2185
2186    coders = ["none", "neon", "sse2", "sse4.1", "avx2"]
2187    parser.add_argument("--encoder", dest="encoder", default="avx2",
2188                        choices=coders, help="test encoder variant")
2189    args = parser.parse_known_args()
2190
2191    # Set the encoder for this test run
2192    g_TestEncoder = args[0].encoder
2193
2194    # Set the sys.argv to remaining args (leave sys.argv[0] alone)
2195    sys.argv[1:] = args[1]
2196
2197    results = unittest.main(exit=False)
2198    return 0 if results.result.wasSuccessful() else 1
2199
2200
2201if __name__ == "__main__":
2202    sys.exit(main())
2203