ssokolow/saveswap

View on GitHub
test_saveswap.py

Summary

Maintainability
A
50 mins
Test Coverage
"""Test suite for saveswap.py

As this relies on helpers from py.test, it must be run with ``py.test``.
"""

from __future__ import (absolute_import, division, print_function,
                        with_statement, unicode_literals)

__author__ = "Stephan Sokolow"
__license__ = "MIT"
__appname__ = "N64-Saveswap"
__version__ = "0.0pre0"

import os, sys
from contextlib import contextmanager

from saveswap import (calculate_padding, byteswap, main, process_path,
                      FileIncomplete, FileTooBig)

@contextmanager
def set_argv(args):
    """Context manager to temporarily modify sys.argv"""
    old_argv = sys.argv
    try:
        sys.argv = [sys.argv[0]] + [str(x) for x in args]
        yield
    finally:
        sys.argv = old_argv

def test_calculate_padding(tmpdir):
    """Test that calculate_padding works as expected"""
    import pytest
    test_file = tmpdir.join("fake_dump")

    for start, expected in (
            (100, 512), (500, 2048), (1000, 32768), (10000, 131072)):
        test_file.write("1234" * start)
        assert calculate_padding(str(test_file)) == expected

    test_file.write("1234" * 100000)
    with pytest.raises(FileTooBig):
        calculate_padding(str(test_file))

def test_byteswap(tmpdir):
    """Test that byteswap produces the expected output"""
    test_file = tmpdir.join("fake_dump")

    # Test the various modes
    for options, expected in (
            ({}, "4321"),
            ({'swap_bytes': False}, "3412"),
            ({'swap_words': False}, "2143"),
            ({'swap_bytes': False, 'swap_words': False}, "1234")):
        test_file.write("1234" * 10)
        byteswap(str(test_file), **options)
        assert test_file.read() == expected * 10

def test_byteswap_padding(tmpdir):
    """Test that byteswap pads as intended"""
    test_file = tmpdir.join("fake_dump")
    test_file.write("1234" * 100000)
    byteswap(str(test_file), pad_to=500000)
    assert test_file.read() == ("4321" * 100000) + ("\x00" * 100000)

def test_byteswap_with_incomplete(tmpdir):
    """Test that byteswap reacts properly to file sizes with remainders

    (ie. file sizes that are not evenly divisible by 2 or 4)
    """
    import pytest
    test_file = tmpdir.join("fake_dump")

    # Define a function which will be called for each combination of inputs
    def test_callback(_bytes, _words, pad_to):
        """Function called many times by _vary_check_swap_inputs"""
        # Test that both types of swapping error out on odd-numbered lengths
        test_file.write("12345")
        if _bytes or _words:
            with pytest.raises(FileIncomplete):
                byteswap(str(test_file), _bytes, _words, pad_to)

        test_file.write("123456")
        if _words:
            with pytest.raises(FileIncomplete):
                byteswap(str(test_file), _bytes, _words, pad_to)
        else:
            byteswap(str(test_file), False, _words, pad_to)

    # Let _vary_check_swap_inputs call test_callback once for each combination
    _vary_check_swap_inputs(test_callback)

def test_process_path_autopad_error(tmpdir):
    """Test that process_path reacts to pad_to=None properly on error"""
    import pytest
    test_file = tmpdir.join("fake_dump")
    backup_path = str(test_file) + '.bak'

    test_file.write("1234" * 100000)
    assert not os.path.exists(backup_path)
    with pytest.raises(FileTooBig):
        process_path(str(test_file), pad_to=None)
    assert test_file.read() == "1234" * 100000  # Unchanged on error
    assert not os.path.exists(backup_path)      # No backup on oversize

def test_process_path_padding(tmpdir):
    """Test that process_path pads properly"""
    test_file = tmpdir.join("fake_dump")
    backup_path = str(test_file) + '.bak'

    test_file.write("1234" * 100000)
    process_path(str(test_file), pad_to=500000)
    assert test_file.read() == ("4321" * 100000) + ("\x00" * 100000)
    assert os.path.exists(backup_path)

def test_process_path_nopad(tmpdir):
    """Test that process_path reacts to pad_to=0 properly"""
    test_file = tmpdir.join("fake_dump")
    backup_path = str(test_file) + '.bak'

    test_file.write("1234" * 100000)
    process_path(str(test_file), pad_to=0)
    assert test_file.read() == "4321" * 100000
    assert os.path.exists(backup_path)

def check_main_retcode(args, code):
    """Helper for testing return codes from main()"""
    try:
        with set_argv(args):
            main()
    except SystemExit as err:
        assert err.code == code

def test_main_works(tmpdir):
    """Functional test for basic main() use"""
    test_file = tmpdir.join("fake_dump")
    backup_path = str(test_file) + '.bak'

    # Test successful runs
    for pat_reps, options, expect_pat, expect_len, backup in (
            (500, [], '4321', 2048, False),
            (100, ['--swap-mode=words-only'], '3412', 512, True),
            (1000, ['--swap-mode=bytes-only'], '2143', 32768, False),
            (1000, ['--force-padding=0',
                    '--swap-mode=bytes-only'], '2143', 4000, False),
            (100000, ['--force-padding=500000'], '4321', 500000, True)):

        bkopt = [] if backup else ['--no-backup']
        test_file.write("1234" * pat_reps)
        with set_argv(options + bkopt + [test_file]):
            main()
        assert test_file.read() == (expect_pat * pat_reps) + (
            "\x00" * (expect_len - (4 * pat_reps)))
        if backup:
            assert os.path.exists(backup_path)
            os.remove(backup_path)

def test_main_missing_file(tmpdir):
    """Functional test for main() with nonexistant path"""
    missing_path = str(tmpdir.join("missing_file"))
    check_main_retcode([missing_path], 10)
    assert not os.path.exists(missing_path + '.bak')

def test_main_error_returns(tmpdir):
    """Functional test for main() with erroring input"""
    test_file = tmpdir.join("fake_dump")
    backup_path = str(test_file) + '.bak'

    assert not os.path.exists(backup_path)
    test_file.write("1234" * 100000)  # Too big
    check_main_retcode([test_file], 20)
    assert not os.path.exists(backup_path)

    test_file.write("12345")          # Not evenly disible by 2
    check_main_retcode([test_file], 30)
    # TODO: Fix the code so the backup is removed on this failure

def _vary_check_swap_inputs(callback):
    """Helper to avoid duplicating stuff within test_byteswap_with_incomplete

    You want to be careful about this, because the number of tests run goes up
    exponentially, but with small numbers of combinations, it's very useful.
    """
    for _bytes in (True, False):
        for _words in (True, False):
            for _padding in (0, 1000, 2048):
                callback(_bytes, _words, _padding)