| #!/usr/bin/env python3 |
| # SPDX-License-Identifier: Apache-2.0 |
| # ----------------------------------------------------------------------------- |
| # Copyright 2020-2026 Arm Limited |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy |
| # of the License at: |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations |
| # under the License. |
| # ----------------------------------------------------------------------------- |
| ''' |
| The functional test runner is a set of tests that validate that the astcenc |
| cmd line tool interface is correctly handled, under both valid and invalid |
| usage scenarios. These tests do not directly validate the correctness of the |
| core codec library, and only the options accessible via the cmd line tool |
| will be exercised. |
| |
| Python PIL does not support enough image formats so, due to the need to |
| validate pixel colors in test images for both LDR and HDR images, these tests |
| rely on an HDRI-enabled build of ImageMagick being available on the system |
| path. To test if the version of ImageMagick on your system is HDRI-enabled run: |
| |
| convert --version (Linux or macOS) |
| magick convert --version (Windows) |
| |
| ... and check that the string 'HDRI' is present in the listed features. |
| |
| Test Tiles |
| ========== |
| |
| Some of these tests use small test images with defined colors. These test tiles |
| are 8x8 texels in size, built up from 4x4 texel blocks of solid color. When |
| compressing with the 4x4 ASTC block size, each 4x4 block can be represented |
| exactly, so we can test for specific colors in tests even though the format is |
| notionally lossy. |
| |
| * See ASTCENC_TEST_PATTERN_LDR for the LDR test pattern colors. |
| * See ASTCENC_TEST_PATTERN_HDR for the HDR test pattern colors. |
| ''' |
| |
| # pylint: disable=too-many-lines |
| |
| import argparse |
| import filecmp |
| import os |
| from pathlib import Path |
| import re |
| import signal |
| import string |
| import subprocess as sp |
| import sys |
| import tempfile |
| import time |
| from typing import Any, Optional |
| import unittest |
| |
| import numpy |
| from PIL import Image |
| |
| import testlib.encoder as te |
| import testlib.image as tli |
| |
| # Enable these to always write out, irrespective of test result |
| ASTCENC_CLI_ALWAYS = False |
| ASTCENC_LOG_ALWAYS = False |
| |
| # Enable these to write out on failure for positive tests |
| ASTCENC_CLI_ON_ERROR = True |
| ASTCENC_LOG_ON_ERROR = True |
| |
| # Enable these to write out on failure for negative tests |
| ASTCENC_CLI_ON_ERROR_NEG = True |
| ASTCENC_LOG_ON_ERROR_NEG = True |
| |
| Color4Type = tuple[float, float, float, float] |
| ColorVType = tuple[float, ...] |
| |
| # LDR test pattern |
| ASTCENC_TEST_PATTERN_LDR = { |
| 'TL': (0.00, 0.00, 0.00, 1.00), |
| 'TR': (1.00, 0.00, 0.00, 1.00), |
| 'BL': (1.00, 1.00, 1.00, 1.00), |
| 'BR': (0.25, 0.75, 0.00, 0.87) |
| } |
| |
| # HDR test pattern |
| ASTCENC_TEST_PATTERN_HDR = { |
| 'TL': (0.00, 0.00, 0.00, 1.00), |
| 'TR': (8.00, 0.00, 0.00, 1.00), |
| 'BL': (3.98, 3.98, 3.98, 1.00), |
| 'BR': (0.25, 0.75, 0.00, 0.87) |
| } |
| |
| LDR_RGB_PSNR_PATTERN = re.compile(r'\s*PSNR \(LDR-RGB\): (.*) dB') |
| |
| |
| class CLITestBase(unittest.TestCase): |
| ''' |
| Command line interface base class. |
| |
| Attributes: |
| binary: The path of the executable to test. |
| ''' |
| test_encoder = 'avx2' |
| |
| def __init__(self, *args, **kwargs): |
| super().__init__(*args, **kwargs) |
| |
| encoder = te.Encoder2x(self.test_encoder) |
| self.binary = encoder.binary_path |
| |
| def setUp(self): |
| ''' |
| Set up a test case. |
| |
| Create a new temporary directory for output files. |
| ''' |
| # pylint: disable=consider-using-with |
| self.tmp_dir = tempfile.TemporaryDirectory() |
| |
| def tearDown(self): |
| ''' |
| Tear down a test case. |
| |
| Clean up the temporary directory created for output files. |
| ''' |
| self.tmp_dir.cleanup() |
| self.tmp_dir = None |
| |
| @staticmethod |
| def get_image_path(image: str) -> Path: |
| ''' |
| Get the path of a specific reference image on disk. |
| |
| Args: |
| image: The image to load. |
| |
| Return: |
| The path to the test image file on disk. |
| ''' |
| script_dir = Path(__file__).parent |
| return script_dir / 'Data' / image |
| |
| @staticmethod |
| def get_ref_image_path(profile: str, mode: str, image: str) -> Path: |
| ''' |
| Get the path of a reference image on disk. |
| |
| Args: |
| profile: The color profile. |
| mode: The type of image to load. |
| image: The image variant to load. |
| |
| Return: |
| The path to the test image file on disk. |
| ''' |
| file_exts = { |
| 'LDR': { |
| 'input': 'png', |
| 'comp': 'astc' |
| }, |
| 'LDRS': { |
| 'input': 'png', |
| 'comp': 'astc' |
| }, |
| 'HDR': { |
| 'input': 'exr', |
| 'comp': 'astc' |
| } |
| } |
| |
| assert profile in file_exts |
| assert mode in file_exts['LDR'] |
| |
| script_dir = Path(__file__).parent |
| file_name = f'{profile}-{image}-1x1.{file_exts[profile][mode]}' |
| return script_dir / 'Data' / file_name |
| |
| def get_tmp_image_path(self, profile: str, mode: str) -> Path: |
| ''' |
| Get the path of a temporary output image on disk. |
| |
| Temporary files are automatically cleaned up when the test tearDown |
| occurs. |
| |
| Args: |
| profile: The color profile. 'EXP' means explicit which means |
| the 'mode' parameter is interpreted as a literal file |
| extension not a symbolic mode. |
| mode: The type of image to load. |
| |
| Return: |
| The path to the test image file on disk. |
| ''' |
| # Handle explicit mode |
| if profile == 'EXP': |
| file_handle, path = tempfile.mkstemp(mode, dir=self.tmp_dir.name) |
| os.close(file_handle) |
| os.remove(path) |
| return Path(path) |
| |
| # Handle symbolic modes |
| file_exts = { |
| 'LDR': { |
| 'comp': '.astc', |
| 'decomp': '.png', |
| 'bad': '.foo' |
| }, |
| 'LDRS': { |
| 'comp': '.astc', |
| 'decomp': '.png', |
| 'bad': '.foo' |
| }, |
| 'HDR': { |
| 'comp': '.astc', |
| 'decomp': '.exr', |
| 'bad': '.foo' |
| } |
| } |
| |
| assert profile in file_exts |
| assert mode in file_exts['LDR'] |
| |
| suffix = file_exts[profile][mode] |
| file_handle, path = tempfile.mkstemp(suffix, dir=self.tmp_dir.name) |
| os.close(file_handle) |
| os.remove(path) |
| return Path(path) |
| |
| |
| class CLIPTest(CLITestBase): |
| ''' |
| Command line interface positive tests. |
| ''' |
| # pylint: disable=too-many-public-methods |
| |
| def compare(self, image1: Path, image2: Path) -> bool: |
| ''' |
| Utility function to compare two images. |
| |
| Note that this comparison tests only decoded color values; file |
| metadata is ignored, and encoding methods are not compared. |
| |
| Args: |
| image1: Path to the first image. |
| image2: Path to the second image. |
| |
| Return: |
| True if the images are the same, False otherwise. |
| ''' |
| # Use Any here because PIL interface doesn't have clean types |
| img1: Any = Image.open(image1) |
| img2: Any = Image.open(image2) |
| |
| # Images must have same size |
| if img1.size != img2.size: |
| return False |
| |
| # Images must have same number of color channels |
| if img1.getbands() != img2.getbands(): |
| # ... unless the only different is alpha |
| self.assertEqual(img1.getbands(), ('R', 'G', 'B')) |
| self.assertEqual(img2.getbands(), ('R', 'G', 'B', 'A')) |
| |
| # ... and the alpha is always one |
| bands = img2.split() |
| alpha_hist = bands[3].histogram() |
| self.assertEqual(sum(alpha_hist[:-1]), 0) |
| |
| # Generate a version of img2 without alpha |
| img2 = Image.merge('RGB', (bands[0], bands[1], bands[2])) |
| |
| # Compute sum of absolute differences |
| dat1 = numpy.array(img1) |
| dat2 = numpy.array(img2) |
| sad = numpy.sum(numpy.abs(dat1 - dat2)) |
| return sad == 0 |
| |
| def get_channel_rmse(self, image1: Path, image2: Path) -> list[float]: |
| ''' |
| Get the channel-by-channel root mean square error. |
| |
| Images must have same channel count. |
| |
| Args: |
| image1: Path to the first image. |
| image2: Path to the second image. |
| |
| Return: |
| List of floats containing the RMSE per channel. |
| ''' |
| # Use Any here because PIL interface doesn't have clean types |
| img1: Any = Image.open(image1) |
| img2: Any = Image.open(image2) |
| |
| # Images must have same size |
| assert img1.size == img2.size |
| |
| # Images must have same number of color channels |
| if img1.getbands() != img2.getbands(): |
| # ... unless the only different is alpha |
| self.assertEqual(img1.getbands(), ('R', 'G', 'B')) |
| self.assertEqual(img2.getbands(), ('R', 'G', 'B', 'A')) |
| |
| # ... and the alpha is always one |
| bands = img2.split() |
| alpha_hist = bands[3].histogram() |
| self.assertEqual(sum(alpha_hist[:-1]), 0) |
| |
| # Generate a version of img2 without alpha |
| img2 = Image.merge('RGB', (bands[0], bands[1], bands[2])) |
| |
| # Compute root mean square error |
| |
| rmse_channels = [] |
| img_channels = zip(img1.split(), img2.split()) |
| for img1_channel, img2_channel in img_channels: |
| texel_count = numpy.prod(img1_channel.size) |
| diff = numpy.array(img1_channel) - numpy.array(img2_channel) |
| sad = numpy.sum(numpy.square(diff)) |
| rmse = numpy.sqrt(numpy.divide(sad, texel_count)) |
| rmse_channels.append(rmse) |
| |
| return rmse_channels |
| |
| @staticmethod |
| def get_color_refs(mode: str, corners: str) -> Color4Type: |
| ''' |
| Get an expected reference color for a tile image corner. |
| |
| Args: |
| mode: The color mode (LDR, or HDR) |
| corners: The corner (TL, TR, BL, or BR) |
| |
| Return: |
| The corner color value. |
| ''' |
| modes = { |
| 'LDR': ASTCENC_TEST_PATTERN_LDR, |
| 'HDR': ASTCENC_TEST_PATTERN_HDR |
| } |
| |
| return modes[mode][corners] |
| |
| def assertColorSame(self, color1: ColorVType, color2: ColorVType, |
| threshold: float = 0.02, |
| swizzle: Optional[str] = None) -> None: |
| ''' |
| Test if a color is the similar to a reference. |
| |
| Will trigger a test failure if the colors are not within threshold. |
| |
| Args: |
| color1: The reference color to compare with. |
| color2: The new color to compare. |
| threshold: The allowed deviation from reference (ratio). |
| swizzle: The swizzle string (4 characters from the set rgba01), to |
| be applied to the reference color. |
| ''' |
| # pylint: disable=invalid-name, consider-using-generator |
| |
| # Two vectors must be same length to be comparable |
| self.assertEqual(len(color1), len(color2)) |
| |
| # Swizzle the reference color if needed |
| if swizzle: |
| self.assertEqual(len(swizzle), len(color1)) |
| |
| # Static remaps for literal zero and 1 |
| remap_index = { |
| '0': len(color1), |
| '1': len(color1) + 1 |
| } |
| |
| # Dynamic remaps for channels that exist in reference |
| if len(color1) >= 1: |
| remap_index['r'] = 0 |
| if len(color1) >= 2: |
| remap_index['g'] = 1 |
| if len(color1) >= 3: |
| remap_index['b'] = 2 |
| if len(color1) >= 4: |
| remap_index['a'] = 3 |
| |
| remap_input = list(color1) + [0.0, 1.0] |
| color1 = tuple([remap_input[remap_index[s]] for s in swizzle]) |
| |
| for ref_channel, new_channel in zip(color1, color2): |
| max_delta = ref_channel * threshold |
| self.assertAlmostEqual(ref_channel, new_channel, delta=max_delta) |
| |
| def exec(self, cmd: list[str | Path], |
| pattern: Optional[re.Pattern] = None) -> str: |
| ''' |
| Execute a positive test. |
| |
| Will trigger a test failure if the subprocess return code is any value |
| other than zero. |
| |
| Args: |
| cmd: The cmd to execute. |
| pattern: A regex pattern to search for, must contain a single group |
| which will be returned to the caller. The test will fail if no |
| pattern match is found. |
| |
| Return: |
| The stdout output of the child process if no pattern, or the first |
| group match from the pattern when applied to stdout output of the |
| child process. |
| ''' |
| try: |
| cmd_str = [str(x) for x in cmd] |
| result = sp.run(cmd_str, stdout=sp.PIPE, stderr=sp.PIPE, |
| text=True, check=True) |
| rcode = result.returncode |
| rstdout = result.stdout |
| rstderr = result.stderr |
| |
| except sp.CalledProcessError as ex: |
| rcode = ex.returncode |
| rstdout = ex.stdout |
| rstderr = ex.stderr |
| |
| passed = rcode == 0 |
| |
| # Emit debug logging if needed |
| if ASTCENC_CLI_ALWAYS or (not passed and ASTCENC_CLI_ON_ERROR): |
| print('\n' + ' '.join(cmd_str)) |
| |
| if ASTCENC_LOG_ALWAYS or (not passed and ASTCENC_LOG_ON_ERROR): |
| print(rstdout) |
| print(rstderr) |
| |
| if rcode < 0: |
| msg = f'Exec died with signal {signal.Signals(-rcode).name}' |
| self.assertGreaterEqual(rcode, 0, msg) |
| |
| if rcode > 0: |
| msg = f'Exec died with application error {rcode}' |
| self.assertEqual(rcode, 0, msg) |
| |
| # If there is a regex pattern provided, then search for it |
| if pattern: |
| if match := pattern.search(rstdout): |
| return match.group(1) |
| self.assertIsNotNone(match) |
| |
| return rstdout |
| |
| def test_ldr_compress(self) -> None: |
| ''' |
| Test basic LDR compression. |
| ''' |
| image_in = self.get_ref_image_path('LDR', 'input', 'A') |
| image_out = self.get_tmp_image_path('LDR', 'comp') |
| image_ref = self.get_ref_image_path('LDR', 'comp', 'A') |
| |
| cmd = [self.binary, '-cl', image_in, image_out, '6x6', '-exhaustive'] |
| self.exec(cmd) |
| self.assertTrue(filecmp.cmp(image_ref, image_out, False)) |
| |
| def test_srgb_compress(self) -> None: |
| ''' |
| Test basic LDR sRGB compression. |
| ''' |
| image_in = self.get_ref_image_path('LDRS', 'input', 'A') |
| image_out = self.get_tmp_image_path('LDRS', 'comp') |
| image_ref = self.get_ref_image_path('LDRS', 'comp', 'A') |
| |
| cmd = [self.binary, '-cs', image_in, image_out, '6x6', '-exhaustive'] |
| self.exec(cmd) |
| self.assertTrue(filecmp.cmp(image_ref, image_out, False)) |
| |
| def test_hdr_compress1(self) -> None: |
| ''' |
| Test basic HDR + LDR alpha compression. |
| ''' |
| image_in = self.get_ref_image_path('HDR', 'input', 'A') |
| image_out = self.get_tmp_image_path('HDR', 'comp') |
| image_ref = self.get_ref_image_path('HDR', 'comp', 'A') |
| |
| cmd = [self.binary, '-ch', image_in, image_out, '6x6', '-exhaustive'] |
| self.exec(cmd) |
| self.assertTrue(filecmp.cmp(image_ref, image_out, False)) |
| |
| def test_hdr_compress2(self) -> None: |
| ''' |
| Test basic HDR + HDR alpha compression. |
| ''' |
| image_in = self.get_ref_image_path('HDR', 'input', 'A') |
| image_out = self.get_tmp_image_path('HDR', 'comp') |
| image_ref = self.get_ref_image_path('HDR', 'comp', 'A') |
| |
| cmd = [self.binary, '-cH', image_in, image_out, '6x6', '-exhaustive'] |
| self.exec(cmd) |
| self.assertTrue(filecmp.cmp(image_ref, image_out, False)) |
| |
| def test_ldr_decompress(self) -> None: |
| ''' |
| Test basic LDR decompression. |
| ''' |
| image_in = self.get_ref_image_path('LDR', 'comp', 'A') |
| image_out = self.get_tmp_image_path('LDR', 'decomp') |
| image_ref = self.get_ref_image_path('LDR', 'input', 'A') |
| |
| cmd = [self.binary, '-dl', image_in, image_out] |
| self.exec(cmd) |
| self.assertTrue(self.compare(image_ref, image_out)) |
| |
| def test_srgb_decompress(self) -> None: |
| ''' |
| Test basic LDR sRGB decompression. |
| ''' |
| image_in = self.get_ref_image_path('LDRS', 'comp', 'A') |
| image_out = self.get_tmp_image_path('LDRS', 'decomp') |
| image_ref = self.get_ref_image_path('LDRS', 'input', 'A') |
| |
| cmd = [self.binary, '-ds', image_in, image_out] |
| self.exec(cmd) |
| self.assertTrue(self.compare(image_ref, image_out)) |
| |
| def test_hdr_decompress1(self) -> None: |
| ''' |
| Test basic HDR + LDR alpha decompression. |
| ''' |
| image_ref = self.get_ref_image_path('HDR', 'input', 'A') |
| image_in = self.get_ref_image_path('HDR', 'comp', 'A') |
| image_out = self.get_tmp_image_path('HDR', 'decomp') |
| |
| cmd = [self.binary, '-dh', image_in, image_out] |
| self.exec(cmd) |
| |
| color_ref = tli.Image(image_ref).get_colors((0, 0)) |
| color_out = tli.Image(image_out).get_colors((0, 0)) |
| self.assertColorSame(color_ref, color_out) |
| |
| def test_hdr_decompress2(self) -> None: |
| ''' |
| Test basic HDR + HDR alpha decompression. |
| ''' |
| image_ref = self.get_ref_image_path('HDR', 'input', 'A') |
| image_in = self.get_ref_image_path('HDR', 'comp', 'A') |
| image_out = self.get_tmp_image_path('HDR', 'decomp') |
| |
| cmd = [self.binary, '-dH', image_in, image_out] |
| self.exec(cmd) |
| |
| color_ref = tli.Image(image_ref).get_colors((0, 0)) |
| color_out = tli.Image(image_out).get_colors((0, 0)) |
| self.assertColorSame(color_ref, color_out) |
| |
| def test_ldr_roundtrip(self) -> None: |
| ''' |
| Test basic LDR round-trip |
| ''' |
| image_in = self.get_ref_image_path('LDR', 'input', 'A') |
| image_out = self.get_tmp_image_path('LDR', 'decomp') |
| |
| cmd = [self.binary, '-tl', image_in, image_out, '6x6', '-exhaustive'] |
| self.exec(cmd) |
| self.assertTrue(self.compare(image_in, image_out)) |
| |
| def test_srgb_roundtrip(self) -> None: |
| ''' |
| Test basic LDR sRGB round-trip |
| ''' |
| image_in = self.get_ref_image_path('LDRS', 'input', 'A') |
| image_out = self.get_tmp_image_path('LDRS', 'decomp') |
| |
| cmd = [self.binary, '-ts', image_in, image_out, '6x6', '-exhaustive'] |
| self.exec(cmd) |
| self.assertTrue(self.compare(image_in, image_out)) |
| |
| def test_hdr_roundtrip1(self) -> None: |
| ''' |
| Test basic HDR + LDR alpha round-trip. |
| ''' |
| image_in = self.get_ref_image_path('HDR', 'input', 'A') |
| image_out = self.get_tmp_image_path('HDR', 'decomp') |
| |
| cmd = [self.binary, '-th', image_in, image_out, '6x6', '-exhaustive'] |
| self.exec(cmd) |
| color_in = tli.Image(image_in).get_colors((0, 0)) |
| color_out = tli.Image(image_out).get_colors((0, 0)) |
| self.assertColorSame(color_in, color_out) |
| |
| def test_hdr_roundtrip2(self) -> None: |
| ''' |
| Test basic HDR + HDR alpha round-trip. |
| ''' |
| image_in = self.get_ref_image_path('HDR', 'input', 'A') |
| image_out = self.get_tmp_image_path('HDR', 'decomp') |
| |
| cmd = [self.binary, '-tH', image_in, image_out, '6x6', '-exhaustive'] |
| self.exec(cmd) |
| color_in = tli.Image(image_in).get_colors((0, 0)) |
| color_out = tli.Image(image_out).get_colors((0, 0)) |
| self.assertColorSame(color_in, color_out) |
| |
| def test_valid_2d_block_sizes(self) -> None: |
| ''' |
| Test all valid block sizes are accepted (2D images). |
| ''' |
| block_sizes = [ |
| '4x4', '5x4', '5x5', '6x5', '6x6', '8x5', '8x6', |
| '10x5', '10x6', '8x8', '10x8', '10x10', '12x10', '12x12' |
| ] |
| |
| image_in = self.get_ref_image_path('LDR', 'input', 'A') |
| image_out = self.get_tmp_image_path('LDR', 'decomp') |
| |
| for block_size in block_sizes: |
| with self.subTest(block_size=block_size): |
| cmd = [ |
| self.binary, '-tl', |
| image_in, image_out, block_size, '-exhaustive' |
| ] |
| |
| self.exec(cmd) |
| color_in = tli.Image(image_in).get_colors((0, 0)) |
| color_out = tli.Image(image_out).get_colors((0, 0)) |
| self.assertColorSame(color_in, color_out) |
| |
| def test_valid_3d_block_sizes(self) -> None: |
| ''' |
| Test all valid block sizes are accepted (3D images). |
| ''' |
| block_sizes = [ |
| '3x3x3', |
| '4x3x3', '4x4x3', '4x4x4', |
| '5x4x4', '5x5x4', '5x5x5', |
| '6x5x5', '6x6x5', '6x6x6' |
| ] |
| |
| image_in = self.get_ref_image_path('LDR', 'input', 'A') |
| image_out = self.get_tmp_image_path('LDR', 'decomp') |
| |
| for block_size in block_sizes: |
| with self.subTest(block_size=block_size): |
| cmd = [ |
| self.binary, '-tl', |
| image_in, image_out, block_size, '-exhaustive' |
| ] |
| |
| self.exec(cmd) |
| color_in = tli.Image(image_in).get_colors((0, 0)) |
| color_out = tli.Image(image_out).get_colors((0, 0)) |
| self.assertColorSame(color_in, color_out) |
| |
| def test_valid_presets(self) -> None: |
| ''' |
| Test all valid presets are accepted |
| ''' |
| presets = ['-fastest', '-fast', '-medium', |
| '-thorough', '-verythorough', '-exhaustive'] |
| |
| image_in = self.get_ref_image_path('LDR', 'input', 'A') |
| image_out = self.get_tmp_image_path('LDR', 'decomp') |
| |
| for preset in presets: |
| with self.subTest(preset=preset): |
| cmd = [self.binary, '-tl', image_in, image_out, '4x4', preset] |
| self.exec(cmd) |
| color_in = tli.Image(image_in).get_colors((0, 0)) |
| color_out = tli.Image(image_out).get_colors((0, 0)) |
| self.assertColorSame(color_in, color_out) |
| |
| def test_valid_ldr_input_formats(self) -> None: |
| ''' |
| Test valid LDR input file formats. |
| ''' |
| image_formats = ['bmp', 'dds', 'jpg', 'ktx', 'png', 'tga'] |
| |
| for image_format in image_formats: |
| with self.subTest(image_format=image_format): |
| image_in = Path(f'./Test/Data/Tiles/ldr.{image_format}') |
| image_out = self.get_tmp_image_path('LDR', 'decomp') |
| |
| cmd = [self.binary, '-tl', image_in, image_out, '4x4', '-fast'] |
| self.exec(cmd) |
| |
| # Check colors if image wrapper supports it |
| if tli.Image.is_format_supported(image_format, 'ldr'): |
| color_in = tli.Image(image_in).get_colors((7, 7)) |
| color_out = tli.Image(image_out).get_colors((7, 7)) |
| |
| # Catch exception and add fallback for tga handling |
| # having unstable origin in ImageMagick |
| try: |
| self.assertColorSame(color_in, color_out) |
| continue |
| except AssertionError as ex: |
| if image_format != 'tga': |
| raise ex |
| |
| # Try yflipped TGA image |
| color_in = tli.Image(image_in).get_colors((7, 7)) |
| color_out = tli.Image(image_out).get_colors((7, 1)) |
| self.assertColorSame(color_in, color_out) |
| |
| def test_valid_uncomp_ldr_output_formats(self) -> None: |
| ''' |
| Test valid uncompressed LDR output file formats. |
| ''' |
| image_formats = ['bmp', 'dds', 'ktx', 'png', 'tga'] |
| |
| for image_format in image_formats: |
| with self.subTest(image_format=image_format): |
| image_in = self.get_ref_image_path('LDR', 'input', 'A') |
| image_out = self.get_tmp_image_path('EXP', f'.{image_format}') |
| |
| cmd = [self.binary, '-tl', image_in, image_out, '4x4', '-fast'] |
| self.exec(cmd) |
| |
| # Check colors if image wrapper supports it |
| if tli.Image.is_format_supported(image_format, 'ldr'): |
| color_in = tli.Image(image_in).get_colors((7, 7)) |
| color_out = tli.Image(image_out).get_colors((7, 7)) |
| self.assertColorSame(color_in, color_out) |
| |
| def test_valid_comp_ldr_output_formats(self) -> None: |
| ''' |
| Test valid compressed LDR output file formats. |
| ''' |
| image_formats = ['astc', 'ktx'] |
| |
| for image_format in image_formats: |
| with self.subTest(image_format=image_format): |
| image_in = self.get_ref_image_path('LDR', 'input', 'A') |
| image_out = self.get_tmp_image_path('EXP', f'.{image_format}') |
| image_out_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| cmd = [self.binary, '-cl', image_in, image_out, '4x4', '-fast'] |
| self.exec(cmd) |
| |
| cmd = [self.binary, '-dl', image_out, image_out_dec] |
| self.exec(cmd) |
| |
| # Check colors if image wrapper supports it |
| if tli.Image.is_format_supported(image_format, 'ldr'): |
| color_in = tli.Image(image_in).get_colors((7, 7)) |
| color_out = tli.Image(image_out_dec).get_colors((7, 7)) |
| self.assertColorSame(color_in, color_out) |
| |
| def test_valid_hdr_input_formats(self) -> None: |
| ''' |
| Test valid HDR input file formats. |
| ''' |
| image_formats = ['exr', 'hdr'] |
| |
| for image_format in image_formats: |
| with self.subTest(image_format=image_format): |
| image_in = Path(f'./Test/Data/Tiles/hdr.{image_format}') |
| image_out = self.get_tmp_image_path('HDR', 'decomp') |
| |
| cmd = [self.binary, '-th', image_in, image_out, '4x4', '-fast'] |
| self.exec(cmd) |
| |
| # Check colors if image wrapper supports it |
| if tli.Image.is_format_supported(image_format, 'hdr'): |
| color_in = tli.Image(image_in).get_colors((7, 7)) |
| color_out = tli.Image(image_out).get_colors((7, 7)) |
| self.assertColorSame(color_in, color_out) |
| |
| def test_valid_uncomp_hdr_output_formats(self) -> None: |
| ''' |
| Test valid uncompressed HDR output file formats. |
| ''' |
| image_formats = ['dds', 'exr', 'hdr', 'ktx'] |
| |
| for image_format in image_formats: |
| with self.subTest(image_format=image_format): |
| image_in = self.get_ref_image_path('HDR', 'input', 'A') |
| image_out = self.get_tmp_image_path('EXP', f'.{image_format}') |
| |
| cmd = [self.binary, '-th', image_in, image_out, '4x4', '-fast'] |
| self.exec(cmd) |
| |
| # Check colors if image wrapper supports it |
| if tli.Image.is_format_supported(image_format, 'hdr'): |
| color_in = tli.Image(image_in).get_colors((7, 7)) |
| color_out = tli.Image(image_out).get_colors((7, 7)) |
| self.assertColorSame(color_in, color_out) |
| |
| def test_valid_comp_hdr_output_formats(self) -> None: |
| ''' |
| Test valid compressed HDR output file formats. |
| ''' |
| image_formats = ['astc', 'ktx'] |
| |
| for image_format in image_formats: |
| with self.subTest(image_format=image_format): |
| image_in = self.get_ref_image_path('HDR', 'input', 'A') |
| image_out = self.get_tmp_image_path('EXP', f'.{image_format}') |
| image_out_dec = self.get_tmp_image_path('HDR', 'decomp') |
| |
| cmd = [self.binary, '-ch', image_in, image_out, '4x4', '-fast'] |
| self.exec(cmd) |
| |
| cmd = [self.binary, '-dh', image_out, image_out_dec] |
| self.exec(cmd) |
| |
| # Check colors if image wrapper supports it |
| if tli.Image.is_format_supported(image_format, 'hdr'): |
| color_in = tli.Image(image_in).get_colors((7, 7)) |
| color_out = tli.Image(image_out_dec).get_colors((7, 7)) |
| self.assertColorSame(color_in, color_out) |
| |
| def test_compress_normal_psnr(self) -> None: |
| ''' |
| Test compression of normal textures using PSNR error metrics. |
| ''' |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| cmd = [ |
| self.binary, '-tl', |
| './Test/Images/Small/LDR-XY/ldr-xy-00.png', |
| image_dec, '5x5', '-exhaustive'] |
| |
| ref_db = float(self.exec(cmd, LDR_RGB_PSNR_PATTERN)) |
| |
| cmd.append('-normal') |
| test_db = float(self.exec(cmd, LDR_RGB_PSNR_PATTERN)) |
| |
| # Note that this test simply asserts that the '-normal_psnr' is |
| # connected and affects the output. We don't test it does something |
| # useful; that it outside the scope of this test case. |
| self.assertNotEqual(ref_db, test_db) |
| |
| def test_compress_normal_percep(self) -> None: |
| ''' |
| Test compression of normal textures using perceptual error metrics. |
| ''' |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| cmd = [ |
| self.binary, '-tl', |
| './Test/Images/Small/LDR-XY/ldr-xy-00.png', |
| image_dec, '4x4', '-exhaustive'] |
| |
| ref_db = float(self.exec(cmd, LDR_RGB_PSNR_PATTERN)) |
| |
| cmd.append('-normal') |
| cmd.append('-perceptual') |
| test_db = float(self.exec(cmd, LDR_RGB_PSNR_PATTERN)) |
| |
| # Note that this test simply asserts that the '-normal -percep' is |
| # connected and affects the output. We don't test it does something |
| # useful; that it outside the scope of this test case. |
| self.assertNotEqual(ref_db, test_db) |
| |
| def test_compress_esw(self) -> None: |
| ''' |
| Test compression swizzles. |
| ''' |
| # The swizzles to test |
| swizzles = ['rgba', 'g0r1', 'rrrg'] |
| |
| # Compress a swizzled image |
| for swizzle in swizzles: |
| with self.subTest(swizzle=swizzle): |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| cmd = [ |
| self.binary, '-tl', |
| './Test/Data/Tiles/ldr.png', |
| image_dec, '4x4', '-exhaustive', |
| '-esw', swizzle] |
| |
| self.exec(cmd) |
| |
| # Fetch the three color |
| img = tli.Image(image_dec) |
| color_test = img.get_colors((7, 7)) |
| color_ref = self.get_color_refs('LDR', 'BR') |
| self.assertColorSame(color_ref, color_test, swizzle=swizzle) |
| |
| def test_compress_dsw(self) -> None: |
| ''' |
| Test decompression swizzles. |
| ''' |
| # The swizzles to test |
| swizzles = ['rgba', 'g0r1', 'rrrg'] |
| |
| # Decompress a swizzled image |
| for swizzle in swizzles: |
| with self.subTest(swizzle=swizzle): |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| cmd = [ |
| self.binary, '-tl', |
| './Test/Data/Tiles/ldr.png', |
| image_dec, '4x4', '-exhaustive', |
| '-dsw', swizzle] |
| |
| self.exec(cmd) |
| |
| # Fetch the three color |
| img = tli.Image(image_dec) |
| color_test = img.get_colors((7, 7)) |
| color_ref = self.get_color_refs('LDR', 'BR') |
| self.assertColorSame(color_ref, color_test, swizzle=swizzle) |
| |
| def test_compress_esw_dsw(self) -> None: |
| ''' |
| Test compression and decompression swizzles |
| ''' |
| # Compress a swizzled image, and swizzle back in decompression |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| cmd = [ |
| self.binary, '-tl', |
| './Test/Data/Tiles/ldr.png', |
| image_dec, '4x4', '-exhaustive', |
| '-esw', 'gbar', '-dsw', 'argb'] |
| |
| self.exec(cmd) |
| |
| # Fetch the three color |
| img = tli.Image(image_dec) |
| color_test = img.get_colors((7, 7)) |
| color_ref = self.get_color_refs('LDR', 'BR') |
| self.assertColorSame(color_ref, color_test) |
| |
| def test_compress_flip(self) -> None: |
| ''' |
| Test LDR image flip on compression. |
| ''' |
| # Compress a flipped image |
| image_comp = self.get_tmp_image_path('LDR', 'comp') |
| |
| cmd = [ |
| self.binary, '-cl', |
| './Test/Data/Tiles/ldr.png', |
| image_comp, '4x4', '-fast', '-yflip'] |
| |
| self.exec(cmd) |
| |
| # Decompress a non-flipped image |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| cmd = [ |
| self.binary, '-dl', |
| image_comp, |
| image_dec] |
| |
| self.exec(cmd) |
| |
| # Compare TL (0, 0) with BL - should match |
| color_ref = self.get_color_refs('LDR', 'BL') |
| |
| img = tli.Image(image_dec) |
| color_test = img.get_colors((0, 0)) |
| self.assertColorSame(color_ref, color_test) |
| |
| def test_decompress_flip(self) -> None: |
| ''' |
| Test LDR image flip on decompression. |
| ''' |
| # Compress a non-flipped image |
| image_comp = self.get_tmp_image_path('LDR', 'comp') |
| |
| cmd = [ |
| self.binary, '-cl', |
| './Test/Data/Tiles/ldr.png', |
| image_comp, '4x4', '-fast'] |
| |
| self.exec(cmd) |
| |
| # Decompress a flipped image |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| cmd = [ |
| self.binary, '-dl', |
| image_comp, |
| image_dec, '-yflip'] |
| |
| self.exec(cmd) |
| |
| # Compare TL (0, 0) with BL - should match |
| color_ref = self.get_color_refs('LDR', 'BL') |
| |
| img = tli.Image(image_dec) |
| color_test = img.get_colors((0, 0)) |
| self.assertColorSame(color_ref, color_test) |
| |
| def test_roundtrip_flip(self) -> None: |
| ''' |
| Test LDR image flip on roundtrip (no flip should occur). |
| ''' |
| # Compress and decompressed a flipped LDR image |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| cmd = [ |
| self.binary, '-tl', |
| './Test/Data/Tiles/ldr.png', |
| image_dec, '4x4', '-fast', '-yflip'] |
| |
| self.exec(cmd) |
| |
| # Compare TL (0, 0) with TL - should match - i.e. no flip |
| color_ref = self.get_color_refs('LDR', 'TL') |
| |
| img = tli.Image(image_dec) |
| color_test = img.get_colors((0, 0)) |
| |
| self.assertColorSame(color_ref, color_test) |
| |
| def test_channel_weighting(self) -> None: |
| ''' |
| Test channel weighting. |
| ''' |
| input_path = Path('./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png') |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| # Compute the basic image without any channel weights |
| cmd = [ |
| self.binary, '-tl', |
| input_path, image_dec, '4x4', '-medium'] |
| |
| self.exec(cmd) |
| rmse_base = self.get_channel_rmse(input_path, image_dec) |
| |
| # Note: Using -cw can result in a worse result than not using -cw, |
| # with regressions in RMSE for the high-weighted channel. This is |
| # particularly an issue in synthetic images, as they are more likely to |
| # hit corner cases in the heuristics. It happens to 'work' for the |
| # selected test image and these settings, but might start to fail in |
| # future due to compressor changes. |
| |
| # Test each channel with a high weight |
| for ch_idx, ch_name in ((0, 'R'), (1, 'G'), (2, 'B'), (3, 'A')): |
| with self.subTest(channel=ch_name): |
| cw_arg = [f'{(10 if x == ch_idx else 1)}' for x in range(0, 4)] |
| cmd2 = cmd + ['-cw'] + cw_arg |
| self.exec(cmd2) |
| rmse_test = self.get_channel_rmse(input_path, image_dec) |
| self.assertLess(rmse_test[ch_idx], rmse_base[ch_idx]) |
| |
| def test_partition_count_limit(self) -> None: |
| ''' |
| Test partition count limit. |
| ''' |
| input_path = Path('./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png') |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| # Compute the basic image without any channel weights |
| cmd = [ |
| self.binary, '-tl', |
| input_path, image_dec, '4x4', '-medium'] |
| |
| self.exec(cmd) |
| rmse_ref = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| cmd += ['-partitioncountlimit', '1'] |
| self.exec(cmd) |
| rmse_test = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| # RMSE should get worse (higher) if we reduce search space |
| self.assertGreater(rmse_test, rmse_ref) |
| |
| def test_2partition_index_limit(self) -> None: |
| ''' |
| Test partition index limit. |
| ''' |
| input_path = Path('./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png') |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| # Compute the basic image without any channel weights |
| cmd = [ |
| self.binary, '-tl', |
| input_path, image_dec, '4x4', '-medium'] |
| |
| self.exec(cmd) |
| rmse_ref = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| cmd += ['-2partitionindexlimit', '1'] |
| self.exec(cmd) |
| rmse_test = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| # RMSE should get worse (higher) if we reduce search space |
| self.assertGreater(rmse_test, rmse_ref) |
| |
| def test_3partition_index_limit(self) -> None: |
| ''' |
| Test partition index limit. |
| ''' |
| input_path = Path('./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png') |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| # Compute the basic image without any channel weights |
| cmd = [ |
| self.binary, '-tl', |
| input_path, image_dec, '4x4', '-medium'] |
| |
| self.exec(cmd) |
| rmse_ref = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| cmd += ['-3partitionindexlimit', '1'] |
| self.exec(cmd) |
| rmse_test = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| # RMSE should get worse (higher) if we reduce search space |
| self.assertGreater(rmse_test, rmse_ref) |
| |
| def test_4partition_index_limit(self) -> None: |
| ''' |
| Test partition index limit. |
| ''' |
| input_path = Path('./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png') |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| # Compute the basic image without any channel weights |
| cmd = [ |
| self.binary, '-tl', |
| input_path, image_dec, '4x4', '-medium'] |
| |
| self.exec(cmd) |
| rmse_ref = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| cmd += ['-4partitionindexlimit', '1'] |
| self.exec(cmd) |
| rmse_test = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| # RMSE should get worse (higher) if we reduce search space |
| self.assertGreater(rmse_test, rmse_ref) |
| |
| def test_blockmode_limit(self) -> None: |
| ''' |
| Test block mode limit. |
| ''' |
| input_path = Path('./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png') |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| # Compute the basic image without any channel weights |
| cmd = [ |
| self.binary, '-tl', |
| input_path, image_dec, '4x4', '-medium'] |
| |
| self.exec(cmd) |
| rmse_ref = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| cmd += ['-blockmodelimit', '25'] |
| self.exec(cmd) |
| rmse_test = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| # RMSE should get worse (higher) if we reduce search space |
| self.assertGreater(rmse_test, rmse_ref) |
| |
| def test_refinement_limit(self) -> None: |
| ''' |
| Test refinement limit. |
| ''' |
| input_path = Path('./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png') |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| cmd = [ |
| self.binary, '-tl', |
| input_path, image_dec, '4x4', '-medium'] |
| |
| self.exec(cmd) |
| rmse_ref = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| cmd += ['-refinementlimit', '1'] |
| self.exec(cmd) |
| rmse_test = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| # RMSE should get worse (higher) if we reduce search space |
| self.assertGreater(rmse_test, rmse_ref) |
| |
| def test_candidate_limit(self) -> None: |
| ''' |
| Test candidate limit. |
| ''' |
| input_path = Path('./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png') |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| cmd = [ |
| self.binary, '-tl', |
| input_path, image_dec, '4x4', '-medium'] |
| |
| self.exec(cmd) |
| rmse_ref = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| cmd += ['-candidatelimit', '1'] |
| self.exec(cmd) |
| rmse_test = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| # RMSE should get worse (higher) if we reduce search space |
| self.assertGreater(rmse_test, rmse_ref) |
| |
| def test_db_cutoff_limit(self) -> None: |
| ''' |
| Test db cutoff limit. |
| ''' |
| input_path = Path('./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png') |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| # Compute the basic image without any channel weights |
| cmd = [ |
| self.binary, '-tl', |
| input_path, image_dec, '4x4', '-medium'] |
| |
| self.exec(cmd) |
| rmse_ref = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| cmd += ['-dblimit', '10'] |
| self.exec(cmd) |
| rmse_test = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| # RMSE should get worse (higher) if we reduce cutoff quality |
| self.assertGreater(rmse_test, rmse_ref) |
| |
| def test_2partition_early_limit(self) -> None: |
| ''' |
| Test 2 partition early limit. |
| ''' |
| input_path = Path('./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png') |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| # Compute the basic image without any channel weights |
| cmd = [ |
| self.binary, '-tl', |
| input_path, image_dec, '4x4', '-medium'] |
| |
| self.exec(cmd) |
| rmse_ref = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| cmd += ['-2partitionlimitfactor', '1.0'] |
| self.exec(cmd) |
| rmse_test = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| # RMSE should get worse (higher) if we reduce search space |
| self.assertGreater(rmse_test, rmse_ref) |
| |
| def test_3partition_early_limit(self) -> None: |
| ''' |
| Test 3 partition early limit. |
| ''' |
| input_path = Path('./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png') |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| # Compute the basic image without any channel weights |
| cmd = [ |
| self.binary, '-tl', |
| input_path, image_dec, '4x4', '-medium'] |
| |
| self.exec(cmd) |
| rmse_ref = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| cmd += ['-3partitionlimitfactor', '1.0'] |
| self.exec(cmd) |
| rmse_test = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| # RMSE should get worse (higher) if we reduce search space |
| self.assertNotEqual(rmse_test, rmse_ref) |
| |
| def test_2plane_correlation_limit(self) -> None: |
| ''' |
| Test 2 plane correlation limit. |
| ''' |
| input_path = Path('./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png') |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| # Compute the basic image without any channel weights |
| cmd = [ |
| self.binary, '-tl', |
| input_path, image_dec, '4x4', '-medium'] |
| |
| self.exec(cmd) |
| rmse_ref = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| cmd += ['-2planelimitcorrelation', '0.1'] |
| self.exec(cmd) |
| rmse_test = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| # RMSE should get worse (higher) if we reduce search space |
| self.assertGreater(rmse_test, rmse_ref) |
| |
| def test_2partition_candidate_limit(self) -> None: |
| ''' |
| Test 2 partition partitioning candidate limit. |
| ''' |
| input_path = Path('./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png') |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| # Compute the basic image without any channel weights |
| cmd = [ |
| self.binary, '-tl', |
| input_path, image_dec, '4x4', '-medium'] |
| |
| self.exec(cmd) |
| rmse_ref = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| cmd += ['-2partitioncandidatelimit', '1'] |
| self.exec(cmd) |
| rmse_test = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| # RMSE should get worse (higher) if we reduce search space |
| self.assertGreater(rmse_test, rmse_ref) |
| |
| def test_3partition_candidate_limit(self) -> None: |
| ''' |
| Test 3 partition partitioning candidate limit. |
| ''' |
| input_path = Path('./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png') |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| # Compute the basic image without any channel weights |
| cmd = [ |
| self.binary, '-tl', |
| input_path, image_dec, '4x4', '-medium'] |
| |
| self.exec(cmd) |
| rmse_ref = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| cmd += ['-3partitioncandidatelimit', '1'] |
| self.exec(cmd) |
| rmse_test = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| # RMSE should get worse (higher) if we reduce search space |
| self.assertGreater(rmse_test, rmse_ref) |
| |
| def test_4partition_candidate_limit(self) -> None: |
| ''' |
| Test 4 partition partitioning candidate limit. |
| ''' |
| input_path = Path('./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png') |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| # Compute the basic image without any channel weights |
| cmd = [ |
| self.binary, '-tl', |
| input_path, image_dec, '4x4', '-medium'] |
| |
| self.exec(cmd) |
| rmse_ref = sum(self.get_channel_rmse(input_path, image_dec)) |
| |
| cmd += ['-4partitioncandidatelimit', '1'] |
| self.exec(cmd) |
| |
| # RMSE should get worse (higher) if we reduce search space |
| # Don't check this here, as 4 partitions not used in any Small image |
| # even for -exhaustive, BUT cmd line option must be accepted and |
| # not error ... |
| del rmse_ref |
| # self.assertGreater(rmse_test, rmse_ref) |
| |
| @unittest.skipIf(os.cpu_count() == 1, 'Cannot test on single core host') |
| def test_thread_count(self) -> None: |
| ''' |
| Test codec thread count. |
| ''' |
| input_path = Path('./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png') |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| # Compute the basic image without any channel weights |
| cmd = [ |
| self.binary, '-tl', |
| input_path, image_dec, '4x4', '-medium'] |
| |
| start = time.time() |
| self.exec(cmd) |
| time_ref = time.time() - start |
| |
| cmd += ['-j', '1'] |
| start = time.time() |
| self.exec(cmd) |
| time_test = time.time() - start |
| |
| # Test time should get slower with fewer threads |
| self.assertGreater(time_test, time_ref) |
| |
| def test_silent(self) -> None: |
| ''' |
| Test silent |
| ''' |
| input_path = Path('./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png') |
| image_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| # Compute the basic image without any channel weights |
| cmd = [ |
| self.binary, '-tl', |
| input_path, image_dec, '4x4', '-medium'] |
| stdout = self.exec(cmd) |
| |
| cmd += ['-silent'] |
| stdout_silent = self.exec(cmd) |
| |
| # Check that stdout is shorter in silent mode. Note that this doesn't |
| # check that it is as silent as it should be, just that silent is wired |
| # somewhere ... |
| self.assertLess(len(stdout_silent), len(stdout)) |
| |
| def test_image_quality_stability(self) -> None: |
| ''' |
| Test that a round-trip and a file-based round-trip give same result. |
| ''' |
| input_path = Path('./Test/Images/Small/LDR-RGBA/ldr-rgba-00.png') |
| image1_dec = self.get_tmp_image_path('LDR', 'decomp') |
| image2_comp = self.get_tmp_image_path('LDR', 'comp') |
| image2_dec = self.get_tmp_image_path('LDR', 'decomp') |
| |
| # Compute the first image using a direct round-trip |
| cmd = [self.binary, '-tl', input_path, image1_dec, '4x4', '-medium'] |
| self.exec(cmd) |
| |
| # Compute the first image using a file-based round-trip |
| cmd = [ |
| self.binary, '-cl', |
| input_path, image2_comp, '4x4', '-medium', |
| '-decode_unorm8' |
| ] |
| |
| self.exec(cmd) |
| |
| cmd = [ |
| self.binary, '-dl', |
| image2_comp, image2_dec |
| ] |
| |
| self.exec(cmd) |
| |
| # RMSE should be the same |
| image1_rmse: float = sum(self.get_channel_rmse(input_path, image1_dec)) |
| image2_rmse: float = sum(self.get_channel_rmse(input_path, image2_dec)) |
| self.assertEqual(image1_rmse, image2_rmse) |
| |
| |
| class CLINTest(CLITestBase): |
| ''' |
| Command line interface negative tests. |
| |
| These tests are designed to test that bad inputs to the cmd line are |
| handled cleanly and that errors are correctly thrown. |
| |
| Note that many tests are mutations of a valid positive test cmd line, |
| to ensure that the base cmd line is valid before it is mutated many |
| of these tests include a *positive test* to ensure that the starting point |
| is actually a valid cmd line (otherwise we could be throwing an |
| arbitrary error). |
| ''' |
| # pylint: disable=too-many-public-methods |
| |
| def exec(self, cmd: list[str | Path], expect_pass: bool = False) -> None: |
| ''' |
| Execute a negative test. |
| |
| Test will automatically fail if: |
| |
| * The subprocess return code is zero, unless expect_pass is True. |
| * The subprocess correctly returned non-zero, but without any error |
| message. |
| * The subprocess dies with any kind of signal. |
| |
| Args: |
| cmd (list(str)): The cmd to execute. |
| expect_pass (bool): True if this cmd is actually expected to |
| pass, which is used to validate commands before mutating them. |
| ''' |
| try: |
| cmd_str = [str(x) for x in cmd] |
| result = sp.run(cmd_str, stdout=sp.PIPE, stderr=sp.PIPE, |
| text=True, check=True) |
| rcode = result.returncode |
| rstdout = result.stdout |
| rstderr = result.stderr |
| |
| except sp.CalledProcessError as ex: |
| rcode = ex.returncode |
| rstdout = ex.stdout |
| rstderr = ex.stderr |
| |
| passed = rcode == 0 |
| |
| # Emit debug logging if needed (negative rcode is a signal) |
| bad_result = (passed != expect_pass) or (rcode < 0) |
| |
| if ASTCENC_CLI_ALWAYS or (bad_result and ASTCENC_CLI_ON_ERROR_NEG): |
| print('\n' + ' '.join(cmd_str)) |
| |
| if ASTCENC_LOG_ALWAYS or (bad_result and ASTCENC_LOG_ON_ERROR_NEG): |
| print(rstdout) |
| print(rstderr) |
| |
| # If we expected a pass, then rcode == 0 |
| if expect_pass: |
| self.assertEqual(rcode, 0, 'Exec did not pass as expected') |
| self.assertNotIn('ERROR', rstdout) |
| self.assertNotIn('ERROR', rstderr) |
| return |
| |
| # If we got a negative that's always bad (signal of some kind) |
| if rcode < 0: |
| msg = f'Exec died with signal {signal.Signals(-rcode).name}' |
| self.assertGreaterEqual(rcode, 0, msg) |
| |
| # Otherwise just assert that we got an error log, and some positive |
| # return code value was returned |
| self.assertIn('ERROR', rstderr) |
| self.assertGreater(rcode, 0, 'Exec did not fail as expected') |
| |
| def exec_with_omit(self, cmd: list[str | Path], omit_from: int) -> None: |
| ''' |
| Execute a negative test with cmd line argument omission. |
| |
| These tests aim to prove that the cmd fails if arguments are |
| missing. However the passed cmd MUST be a valid cmd which |
| passes if no argument are omitted (this is checked, to ensure that |
| the test case is a valid test). |
| |
| Test will automatically fail if: |
| |
| * A partial cmd doesn't fail. |
| * The full cmd doesn't pass. |
| |
| Args: |
| cmd: The command to run. |
| omit_from: The index at which to start dropping arguments. |
| ''' |
| # Run the cmd, incrementally omitting arguments |
| cmd_len = len(cmd) |
| for omit_index in range(omit_from, cmd_len + 1): |
| omit = cmd_len - omit_index |
| with self.subTest(omit=omit): |
| cmd_modified = cmd[:omit_index] |
| expect_pass = omit == 0 |
| self.exec(cmd_modified, expect_pass) |
| |
| def test_cl_missing_args(self) -> None: |
| ''' |
| Test -cl with missing arguments. |
| ''' |
| # Build a valid cmd |
| cmd = [ |
| self.binary, '-cl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fast'] |
| |
| self.exec_with_omit(cmd, 2) |
| |
| def test_cl_missing_input(self) -> None: |
| ''' |
| Test -cl with a missing input file. |
| ''' |
| # Build a valid cmd with a missing input file |
| cmd = [ |
| self.binary, '-cl', |
| './Test/Data/missing.png', |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fast'] |
| |
| self.exec(cmd) |
| |
| def test_cl_missing_input_array_slice(self) -> None: |
| ''' |
| Test -cl with a missing input file in an array slice. |
| ''' |
| # Build a valid cmd with a missing input file |
| cmd = [ |
| self.binary, '-cl', |
| './Test/Data/Tiles/ldr.png', |
| self.get_tmp_image_path('LDR', 'comp'), |
| '3x3x3', '-fast', '-zdim', '3'] |
| |
| self.exec(cmd) |
| |
| def test_cl_unknown_input(self) -> None: |
| ''' |
| Test -cl with an unknown input file extension. |
| ''' |
| # Build an otherwise valid cmd with the test flaw |
| cmd = [ |
| self.binary, '-cl', |
| './Test/Data/empty.unk', |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fast'] |
| |
| self.exec(cmd) |
| |
| def test_cl_missing_output(self) -> None: |
| ''' |
| Test -cl with a missing output directory. |
| ''' |
| # Build an otherwise valid cmd with the test flaw |
| cmd = [ |
| self.binary, '-cl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| './DoesNotExist/test.astc', |
| '4x4', '-fast'] |
| |
| self.exec(cmd) |
| |
| def test_cl_unknown_output(self) -> None: |
| ''' |
| Test -cl with an unknown output file extension. |
| ''' |
| # Build an otherwise valid cmd with the test flaw |
| cmd = [ |
| self.binary, '-cl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| './test.aastc', |
| '4x4', '-fast'] |
| |
| self.exec(cmd) |
| |
| def test_cl_bad_block_size(self) -> None: |
| ''' |
| Test -cl with an invalid block size. |
| ''' |
| bad_sizes = [ |
| '4x5', # Illegal 2D block size |
| '3x3x4', # Illegal 3D block size |
| '4x4x4x4', # Too many dimensions |
| '4x', # Incomplete 2D block size |
| '4x4x', # Incomplete 3D block size |
| '4x4x4x', # Over-long 3D block size |
| '4xe', # Illegal non-numeric character |
| '4x4e' # Additional non-numeric character |
| ] |
| |
| # Build an otherwise valid cmd with the test flaw |
| cmd = [ |
| self.binary, '-cl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fast'] |
| |
| # Test that the underlying cmd is valid |
| self.exec(cmd, True) |
| |
| block_index = cmd.index('4x4') |
| for bad_size in bad_sizes: |
| with self.subTest(block_size=bad_size): |
| cmd[block_index] = bad_size |
| self.exec(cmd) |
| |
| def test_cl_bad_preset(self) -> None: |
| ''' |
| Test -cl with an invalid encoding preset. |
| ''' |
| # Build an otherwise valid cmd with the test flaw |
| cmd = [ |
| self.binary, '-cl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fastt'] |
| |
| self.exec(cmd) |
| |
| def test_cl_bad_argument(self) -> None: |
| ''' |
| Test -cl with an unknown additional argument. |
| ''' |
| # Build an otherwise valid cmd with the test flaw |
| cmd = [ |
| self.binary, '-cl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fast', '-unknown'] |
| |
| self.exec(cmd) |
| |
| def test_cl_2d_block_with_array(self) -> None: |
| ''' |
| Test -cl with a 2D block size and 3D input data. |
| ''' |
| # Build an otherwise valid cmd with the test flaw |
| |
| cmd = [ |
| self.binary, '-cl', |
| './Test/Data/Tiles/ldr.png', |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fast', '-zdim', '2'] |
| |
| self.exec(cmd) |
| |
| def test_cl_array_missing_args(self) -> None: |
| ''' |
| Test -cl with a 2D block size and 3D input data. |
| ''' |
| # Build an otherwise valid cmd |
| cmd = [ |
| self.binary, '-cl', |
| './Test/Data/Tiles/ldr.png', |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4x4', '-fast', '-zdim', '2'] |
| |
| # Run the cmd, incrementally omitting arguments |
| self.exec_with_omit(cmd, 7) |
| |
| def test_tl_missing_args(self) -> None: |
| ''' |
| Test -tl with missing arguments. |
| ''' |
| # Build a valid cmd |
| cmd = [ |
| self.binary, '-tl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'decomp'), |
| '4x4', '-fast'] |
| |
| # Run the cmd, incrementally omitting arguments |
| self.exec_with_omit(cmd, 2) |
| |
| def test_tl_missing_input(self) -> None: |
| ''' |
| Test -tl with a missing input file. |
| ''' |
| # Build a valid cmd with a missing input file |
| cmd = [ |
| self.binary, '-tl', |
| './Test/Data/missing.png', |
| self.get_tmp_image_path('LDR', 'decomp'), |
| '4x4', '-fast'] |
| |
| self.exec(cmd) |
| |
| def test_tl_unknown_input(self) -> None: |
| ''' |
| Test -tl with an unknown input file extension. |
| ''' |
| # Build an otherwise valid cmd with the test flaw |
| cmd = [ |
| self.binary, '-tl', |
| './Test/Data/empty.unk', |
| self.get_tmp_image_path('LDR', 'decomp'), |
| '4x4', '-fast'] |
| |
| self.exec(cmd) |
| |
| def test_tl_missing_output(self) -> None: |
| ''' |
| Test -tl with a missing output directory. |
| ''' |
| # Build an otherwise valid cmd with the test flaw |
| cmd = [ |
| self.binary, '-tl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| './DoesNotExist/test.png', |
| '4x4', '-fast'] |
| |
| self.exec(cmd) |
| |
| def test_tl_bad_block_size(self) -> None: |
| ''' |
| Test -tl with an invalid block size. |
| ''' |
| bad_sizes = [ |
| '4x5', # Illegal 2D block size |
| '3x3x4', # Illegal 3D block size |
| '4x4x4x4', # Too many dimensions |
| '4x', # Incomplete 2D block size |
| '4x4x', # Incomplete 3D block size |
| '4x4x4x', # Over-long 3D block size |
| '4xe', # Illegal non-numeric character |
| '4x4e' # Additional non-numeric character |
| ] |
| |
| # Build an otherwise valid cmd with the test flaw |
| cmd = [ |
| self.binary, '-tl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'decomp'), |
| '4x4', '-fast'] |
| |
| # Test that the underlying cmd is valid |
| self.exec(cmd, True) |
| |
| block_index = cmd.index('4x4') |
| for bad_size in bad_sizes: |
| with self.subTest(block_size=bad_size): |
| cmd[block_index] = bad_size |
| self.exec(cmd) |
| |
| def test_tl_bad_preset(self) -> None: |
| ''' |
| Test -tl with an invalid encoding preset. |
| ''' |
| # Build an otherwise valid cmd with the test flaw |
| cmd = [ |
| self.binary, '-tl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'decomp'), |
| '4x4', '-fastt'] |
| |
| self.exec(cmd) |
| |
| def test_tl_bad_argument(self) -> None: |
| ''' |
| Test -tl with an unknown additional argument. |
| ''' |
| # Build an otherwise valid cmd with the test flaw |
| cmd = [ |
| self.binary, '-tl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'decomp'), |
| '4x4', '-fast', '-unknown'] |
| |
| self.exec(cmd) |
| |
| def test_dl_missing_args(self) -> None: |
| ''' |
| Test -dl with missing arguments. |
| ''' |
| # Build a valid cmd |
| cmd = [ |
| self.binary, '-dl', |
| self.get_ref_image_path('LDR', 'comp', 'A'), |
| self.get_tmp_image_path('LDR', 'decomp')] |
| |
| # Run the cmd, incrementally omitting arguments |
| self.exec_with_omit(cmd, 2) |
| |
| def test_dl_missing_output(self) -> None: |
| ''' |
| Test -dl with a missing output directory. |
| ''' |
| # Build an otherwise valid cmd with the test flaw |
| cmd = [ |
| self.binary, '-dl', |
| self.get_ref_image_path('LDR', 'comp', 'A'), |
| './DoesNotExist/test.png'] |
| |
| self.exec(cmd) |
| |
| def test_cl_a_missing_args(self) -> None: |
| ''' |
| Test -cl with -a and missing arguments. |
| ''' |
| # Build a valid cmd |
| cmd = [ |
| self.binary, '-cl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fast', |
| '-a', '2'] |
| |
| # Run the cmd, incrementally omitting arguments |
| self.exec_with_omit(cmd, 7) |
| |
| def test_cl_cw_missing_args(self) -> None: |
| ''' |
| Test -cl with -cw and missing arguments. |
| ''' |
| # Build a valid cmd |
| cmd = [ |
| self.binary, '-cl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fast', |
| '-cw', '0', '1', '2', '3'] |
| |
| # Run the cmd, incrementally omitting arguments |
| self.exec_with_omit(cmd, 7) |
| |
| def test_cl_2partitionindexlimit_missing_args(self) -> None: |
| ''' |
| Test -cl with -2partitionindexlimit and missing arguments. |
| ''' |
| # Build a valid cmd |
| cmd = [ |
| self.binary, '-cl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fast', |
| '-2partitionindexlimit', '3'] |
| |
| # Run the cmd, incrementally omitting arguments |
| self.exec_with_omit(cmd, 7) |
| |
| def test_cl_3partitionindexlimit_missing_args(self) -> None: |
| ''' |
| Test -cl with -3partitionindexlimit and missing arguments. |
| ''' |
| # Build a valid cmd |
| cmd = [ |
| self.binary, '-cl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fast', |
| '-3partitionindexlimit', '3'] |
| |
| # Run the cmd, incrementally omitting arguments |
| self.exec_with_omit(cmd, 7) |
| |
| def test_cl_4partitionindexlimit_missing_args(self) -> None: |
| ''' |
| Test -cl with -4partitionindexlimit and missing arguments. |
| ''' |
| # Build a valid cmd |
| cmd = [ |
| self.binary, '-cl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fast', |
| '-4partitionindexlimit', '3'] |
| |
| # Run the cmd, incrementally omitting arguments |
| self.exec_with_omit(cmd, 7) |
| |
| def test_cl_2partitioncandidatelimit_missing_args(self) -> None: |
| ''' |
| Test -cl with -2partitioncandidatelimit and missing arguments. |
| ''' |
| # Build a valid cmd |
| cmd = [ |
| self.binary, '-cl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fast', |
| '-2partitioncandidatelimit', '1'] |
| |
| # Run the cmd, incrementally omitting arguments |
| self.exec_with_omit(cmd, 7) |
| |
| def test_cl_3partitioncandidatelimit_missing_args(self) -> None: |
| ''' |
| Test -cl with -3partitioncandidatelimit and missing arguments. |
| ''' |
| # Build a valid cmd |
| cmd = [ |
| self.binary, '-cl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fast', |
| '-3partitioncandidatelimit', '3'] |
| |
| # Run the cmd, incrementally omitting arguments |
| self.exec_with_omit(cmd, 7) |
| |
| def test_cl_4partitioncandidatelimit_missing_args(self) -> None: |
| ''' |
| Test -cl with -4partitioncandidatelimit and missing arguments. |
| ''' |
| # Build a valid cmd |
| cmd = [ |
| self.binary, '-cl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fast', |
| '-4partitioncandidatelimit', '3'] |
| |
| # Run the cmd, incrementally omitting arguments |
| self.exec_with_omit(cmd, 7) |
| |
| def test_cl_blockmodelimit_missing_args(self) -> None: |
| ''' |
| Test -cl with -blockmodelimit and missing arguments. |
| ''' |
| # Build a valid cmd |
| cmd = [ |
| self.binary, '-cl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fast', |
| '-blockmodelimit', '3'] |
| |
| # Run the cmd, incrementally omitting arguments |
| self.exec_with_omit(cmd, 7) |
| |
| def test_cl_refinementlimit_missing_args(self) -> None: |
| ''' |
| Test -cl with -refinementlimit and missing arguments. |
| ''' |
| # Build a valid cmd |
| cmd = [ |
| self.binary, '-cl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fast', |
| '-refinementlimit', '3'] |
| |
| # Run the cmd, incrementally omitting arguments |
| self.exec_with_omit(cmd, 7) |
| |
| def test_cl_dblimit_missing_args(self) -> None: |
| ''' |
| Test -cl with -dblimit and missing arguments. |
| ''' |
| # Build a valid cmd |
| cmd = [ |
| self.binary, '-cl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fast', |
| '-dblimit', '3'] |
| |
| # Run the cmd, incrementally omitting arguments |
| self.exec_with_omit(cmd, 7) |
| |
| def test_cl_2partitionearlylimit_missing_args(self) -> None: |
| ''' |
| Test -cl with -2partitionlimitfactor and missing arguments. |
| ''' |
| # Build a valid cmd |
| cmd = [ |
| self.binary, '-cl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fast', |
| '-2partitionlimitfactor', '3'] |
| |
| # Run the cmd, incrementally omitting arguments |
| self.exec_with_omit(cmd, 7) |
| |
| def test_cl_3partitionearlylimit_missing_args(self) -> None: |
| ''' |
| Test -cl with -3partitionlimitfactor and missing arguments. |
| ''' |
| # Build a valid cmd |
| cmd = [ |
| self.binary, '-cl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fast', |
| '-3partitionlimitfactor', '3'] |
| |
| # Run the cmd, incrementally omitting arguments |
| self.exec_with_omit(cmd, 7) |
| |
| def test_cl_2planeearlylimit_missing_args(self) -> None: |
| ''' |
| Test -cl with -2planelimitcorrelation and missing arguments. |
| ''' |
| # Build a valid cmd |
| cmd = [ |
| self.binary, '-cl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fast', |
| '-2planelimitcorrelation', '0.66'] |
| |
| # Run the cmd, incrementally omitting arguments |
| self.exec_with_omit(cmd, 7) |
| |
| def test_cl_esw_missing_args(self) -> None: |
| ''' |
| Test -cl with -esw and missing arguments. |
| ''' |
| # Build a valid cmd |
| cmd = [ |
| self.binary, '-cl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fast', |
| '-esw', 'rgb1'] |
| |
| # Run the cmd, incrementally omitting arguments |
| self.exec_with_omit(cmd, 7) |
| |
| def test_cl_esw_invalid_swizzle(self) -> None: |
| ''' |
| Test -cl with -esw and invalid swizzles. |
| ''' |
| bad_swizzles = [ |
| '', # Short swizzles |
| 'r', |
| 'rr', |
| 'rrr', |
| 'rrrrr', # Long swizzles |
| ] |
| |
| # Create swizzles with all invalid printable ascii codes |
| good = ['r', 'g', 'b', 'a', '0', '1'] |
| for channel in string.printable: |
| if channel not in good: |
| bad_swizzles.append(channel * 4) |
| |
| # Build a valid base cmd |
| cmd = [ |
| self.binary, '-cl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fast', |
| '-esw', 'rgba'] |
| |
| block_index = cmd.index('rgba') |
| for bad_swizzle in bad_swizzles: |
| with self.subTest(swizzle=bad_swizzle): |
| cmd[block_index] = bad_swizzle |
| self.exec(cmd) |
| |
| def test_cl_ssw_missing_args(self) -> None: |
| ''' |
| Test -cl with -ssw and missing arguments. |
| ''' |
| # Build a valid cmd |
| cmd = [ |
| self.binary, '-cl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fast', |
| '-ssw', 'rgba'] |
| |
| # Run the cmd, incrementally omitting arguments |
| self.exec_with_omit(cmd, 7) |
| |
| def test_cl_ssw_invalid_swizzle(self) -> None: |
| ''' |
| Test -cl with -ssw and invalid swizzles. |
| ''' |
| bad_swizzles = [ |
| '', # Short swizzles |
| 'rrrrr', # Long swizzles |
| ] |
| |
| # Create swizzles with all invalid printable ascii codes |
| good = ['r', 'g', 'b', 'a'] |
| for channel in string.printable: |
| if channel not in good: |
| bad_swizzles.append(channel * 4) |
| |
| # Build a valid base cmd |
| cmd = [ |
| self.binary, '-cl', |
| self.get_ref_image_path('LDR', 'input', 'A'), |
| self.get_tmp_image_path('LDR', 'comp'), |
| '4x4', '-fast', |
| '-ssw', 'rgba'] |
| |
| block_index = cmd.index('rgba') |
| for bad_swizzle in bad_swizzles: |
| with self.subTest(swizzle=bad_swizzle): |
| cmd[block_index] = bad_swizzle |
| self.exec(cmd) |
| |
| def test_dl_dsw_missing_args(self) -> None: |
| ''' |
| Test -dl with -dsw and missing arguments. |
| ''' |
| # Build a valid cmd |
| cmd = [ |
| self.binary, '-dl', |
| self.get_ref_image_path('LDR', 'comp', 'A'), |
| self.get_tmp_image_path('LDR', 'decomp'), |
| '-dsw', 'rgb1'] |
| |
| # Run the cmd, incrementally omitting arguments |
| self.exec_with_omit(cmd, 5) |
| |
| def test_dl_dsw_invalid_swizzle(self) -> None: |
| ''' |
| Test -dl with -dsw and invalid swizzles. |
| ''' |
| bad_swizzles = [ |
| '', # Short swizzles |
| 'r', |
| 'rr', |
| 'rrr', |
| 'rrrrr', # Long swizzles |
| ] |
| |
| # Create swizzles with all invalid printable ascii codes |
| good = ['r', 'g', 'b', 'a', 'z', '0', '1'] |
| for channel in string.printable: |
| if channel not in good: |
| bad_swizzles.append(channel * 4) |
| |
| # Build a valid base cmd |
| cmd = [ |
| self.binary, '-dl', |
| self.get_ref_image_path('LDR', 'comp', 'A'), |
| self.get_tmp_image_path('LDR', 'decomp'), |
| '-dsw', 'rgba'] |
| |
| block_index = cmd.index('rgba') |
| for bad_swizzle in bad_swizzles: |
| with self.subTest(swizzle=bad_swizzle): |
| cmd[block_index] = bad_swizzle |
| self.exec(cmd) |
| |
| def test_ch_mpsnr_missing_args(self) -> None: |
| ''' |
| Test -ch with -mpsnr and missing arguments. |
| ''' |
| # Build a valid cmd |
| cmd = [ |
| self.binary, '-ch', |
| self.get_ref_image_path('HDR', 'input', 'A'), |
| self.get_tmp_image_path('HDR', 'comp'), |
| '4x4', '-fast', |
| '-mpsnr', '-5', '5'] |
| |
| # Run the cmd, incrementally omitting arguments |
| self.exec_with_omit(cmd, 7) |
| |
| def test_dl_corrupt_astc_magic(self) -> None: |
| ''' |
| Test -dl with an astc file with the wrong magic number. |
| ''' |
| # Build a valid cmd with a bad image file |
| cmd = [ |
| self.binary, '-dl', |
| self.get_image_path('negative_magic.astc'), |
| self.get_tmp_image_path('LDR', 'decomp')] |
| |
| self.exec(cmd) |
| |
| def test_dl_corrupt_astc_huge_size(self) -> None: |
| ''' |
| Test -dl with an astc file with a data size that will fail alloc. |
| |
| Note this test will cause ASAN to error because ASAN itself cannot cope |
| with OOM issues. ASAN documentation says you can run with environment |
| ASAN_OPTIONS=allocator_may_return_null=1, but this doesn't seem to work |
| for allocations made with C++ new[]. This test will need to be disabled |
| when ASAN is used. |
| ''' |
| # Build a valid cmd with a bad image file |
| cmd = [ |
| self.binary, '-dl', |
| self.get_image_path('negative_huge.astc'), |
| self.get_tmp_image_path('LDR', 'decomp')] |
| |
| self.exec(cmd) |
| |
| def test_dl_corrupt_astc_overflow_size(self) -> None: |
| ''' |
| Test -dl with an astc file with a total data size that overflows. |
| ''' |
| # Build a valid cmd with a bad image file |
| cmd = [ |
| self.binary, '-dl', |
| self.get_image_path('negative_overflow.astc'), |
| self.get_tmp_image_path('LDR', 'decomp')] |
| |
| self.exec(cmd) |
| |
| def test_dl_corrupt_astc_short_data(self) -> None: |
| ''' |
| Test -dl with an astc file missing data. |
| ''' |
| # Build a valid cmd with a bad image file |
| cmd = [ |
| self.binary, '-dl', |
| self.get_image_path('negative_short.astc'), |
| self.get_tmp_image_path('LDR', 'decomp')] |
| |
| self.exec(cmd) |
| |
| def test_dl_corrupt_astc_bad_block_size(self) -> None: |
| ''' |
| Test -dl with an astc file with invalid block size. |
| ''' |
| # Build a valid cmd with a bad image file |
| cmd = [ |
| self.binary, '-dl', |
| self.get_image_path('negative_block_size.astc'), |
| self.get_tmp_image_path('LDR', 'decomp')] |
| |
| self.exec(cmd) |
| |
| |
| def main() -> int: |
| ''' |
| The main function. |
| |
| Return: |
| The process return code. |
| ''' |
| parser = argparse.ArgumentParser() |
| |
| coders = ['none', 'neon', 'sve_128', 'sve_256', 'sse2', 'sse4.1', 'avx2'] |
| parser.add_argument('--encoder', dest='encoder', default='avx2', |
| choices=coders, help='test encoder variant') |
| args = parser.parse_known_args() |
| |
| # Set the encoder for this test run |
| CLITestBase.test_encoder = args[0].encoder |
| |
| # Set the sys.argv to remaining args (leave sys.argv[0] alone) |
| sys.argv[1:] = args[1] |
| |
| results = unittest.main(exit=False) |
| return 0 if results.result.wasSuccessful() else 1 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |