#!/usr/bin/env python3
#
# strided.py
#
# Test zonemode=strided. This uses the null ioengine when no file is
# specified. If a file is specified, use it for randdom read testing.
# Some of the zoneranges in the tests are 16MiB. So when using a file
# a minimum size of 32MiB is recommended.
#
# USAGE
# python strided.py fio-executable [-f file/device]
#
# EXAMPLES
# python t/strided.py ./fio
# python t/strided.py ./fio -f /dev/sda
# dd if=/dev/zero of=temp bs=1M count=32
# python t/strided.py ./fio -f temp
#
# REQUIREMENTS
# Python 2.6+
#
# ===TEST MATRIX===
#
# --zonemode=strided, zoneskip unset
#   w/ randommap and LFSR
#       zonesize=zonerange  all blocks in zonerange touched
#       zonesize>zonerange  all blocks touched and roll-over back into zone
#       zonesize<zonerange  all blocks inside zone
#
#   w/o randommap       all blocks inside zone
#

from __future__ import absolute_import
from __future__ import print_function
import os
import sys
import argparse
import subprocess


def parse_args():
    parser = argparse.ArgumentParser()
    parser.add_argument('fio',
                        help='path to fio executable (e.g., ./fio)')
    parser.add_argument('-f', '--filename', help="file/device to test")
    args = parser.parse_args()

    return args


def run_fio(fio, test, index):
    filename = "strided"
    fio_args = [
                "--max-jobs=16",
                "--name=strided",
                "--zonemode=strided",
                "--log_offset=1",
                "--randrepeat=0",
                "--rw=randread",
                "--write_iops_log={0}{1:03d}".format(filename, index),
                "--output={0}{1:03d}.out".format(filename, index),
                "--zonerange={zonerange}".format(**test),
                "--zonesize={zonesize}".format(**test),
                "--bs={bs}".format(**test),
               ]
    if 'norandommap' in test:
        fio_args.append('--norandommap')
    if 'random_generator' in test:
        fio_args.append('--random_generator={random_generator}'.format(**test))
    if 'offset' in test:
        fio_args.append('--offset={offset}'.format(**test))
    if 'filename' in test:
        fio_args.append('--filename={filename}'.format(**test))
        fio_args.append('--filesize={filesize})'.format(**test))
    else:
        fio_args.append('--ioengine=null')
        fio_args.append('--size={size}'.format(**test))
        fio_args.append('--io_size={io_size}'.format(**test))
        fio_args.append('--filesize={size})'.format(**test))

    output = subprocess.check_output([fio] + fio_args, universal_newlines=True)

    f = open("{0}{1:03d}_iops.1.log".format(filename, index), "r")
    log = f.read()
    f.close()

    return log


def check_output(iops_log, test):
    zonestart = 0 if 'offset' not in test else test['offset']
    iospersize = test['zonesize'] / test['bs']
    iosperrange = test['zonerange'] / test['bs']
    iosperzone = 0
    lines = iops_log.split('\n')
    zoneset = set()

    for line in lines:
        if len(line) == 0:
            continue

        if iosperzone == iospersize:
            # time to move to a new zone
            iosperzone = 0
            zoneset = set()
            zonestart += test['zonerange']
            if zonestart >= test['filesize']:
                zonestart = 0 if 'offset' not in test else test['offset']

        iosperzone = iosperzone + 1
        tokens = line.split(',')
        offset = int(tokens[4])
        if offset < zonestart or offset >= zonestart + test['zonerange']:
            print("Offset {0} outside of zone starting at {1}".format(
                    offset, zonestart))
            return False

        # skip next section if norandommap is enabled with no
        # random_generator or with a random_generator != lfsr
        if 'norandommap' in test:
            if 'random_generator' in test:
                if test['random_generator'] != 'lfsr':
                    continue
            else:
                continue

        # we either have a random map enabled or we
        # are using an LFSR
        # so all blocks should be unique and we should have
        # covered the entire zone when iosperzone % iosperrange == 0
        block = (offset - zonestart) / test['bs']
        if block in zoneset:
            print("Offset {0} in zone already touched".format(offset))
            return False

        zoneset.add(block)
        if iosperzone % iosperrange == 0:
            if len(zoneset) != iosperrange:
                print("Expected {0} blocks in zone but only saw {1}".format(
                        iosperrange, len(zoneset)))
                return False
            zoneset = set()

    return True


if __name__ == '__main__':
    args = parse_args()

    tests = [   # randommap enabled
                {
                    "zonerange": 4096,
                    "zonesize": 4096,
                    "bs": 4096,
                    "offset": 8*4096,
                    "size": 16*4096,
                    "io_size": 16*4096,
                },
                {
                    "zonerange": 4096,
                    "zonesize": 4096,
                    "bs": 4096,
                    "size": 16*4096,
                    "io_size": 16*4096,
                },
                {
                    "zonerange": 16*1024*1024,
                    "zonesize": 16*1024*1024,
                    "bs": 4096,
                    "size": 256*1024*1024,
                    "io_size": 256*1024*204,
                },
                {
                    "zonerange": 4096,
                    "zonesize": 4*4096,
                    "bs": 4096,
                    "size": 16*4096,
                    "io_size": 16*4096,
                },
                {
                    "zonerange": 16*1024*1024,
                    "zonesize": 32*1024*1024,
                    "bs": 4096,
                    "size": 256*1024*1024,
                    "io_size": 256*1024*204,
                },
                {
                    "zonerange": 8192,
                    "zonesize": 4096,
                    "bs": 4096,
                    "size": 16*4096,
                    "io_size": 16*4096,
                },
                {
                    "zonerange": 16*1024*1024,
                    "zonesize": 8*1024*1024,
                    "bs": 4096,
                    "size": 256*1024*1024,
                    "io_size": 256*1024*204,
                },
                # lfsr
                {
                    "random_generator": "lfsr",
                    "zonerange": 4096*1024,
                    "zonesize": 4096*1024,
                    "bs": 4096,
                    "offset": 8*4096*1024,
                    "size": 16*4096*1024,
                    "io_size": 16*4096*1024,
                },
                {
                    "random_generator": "lfsr",
                    "zonerange": 4096*1024,
                    "zonesize": 4096*1024,
                    "bs": 4096,
                    "size": 16*4096*1024,
                    "io_size": 16*4096*1024,
                },
                {
                    "random_generator": "lfsr",
                    "zonerange": 16*1024*1024,
                    "zonesize": 16*1024*1024,
                    "bs": 4096,
                    "size": 256*1024*1024,
                    "io_size": 256*1024*204,
                },
                {
                    "random_generator": "lfsr",
                    "zonerange": 4096*1024,
                    "zonesize": 4*4096*1024,
                    "bs": 4096,
                    "size": 16*4096*1024,
                    "io_size": 16*4096*1024,
                },
                {
                    "random_generator": "lfsr",
                    "zonerange": 16*1024*1024,
                    "zonesize": 32*1024*1024,
                    "bs": 4096,
                    "size": 256*1024*1024,
                    "io_size": 256*1024*204,
                },
                {
                    "random_generator": "lfsr",
                    "zonerange": 8192*1024,
                    "zonesize": 4096*1024,
                    "bs": 4096,
                    "size": 16*4096*1024,
                    "io_size": 16*4096*1024,
                },
                {
                    "random_generator": "lfsr",
                    "zonerange": 16*1024*1024,
                    "zonesize": 8*1024*1024,
                    "bs": 4096,
                    "size": 256*1024*1024,
                    "io_size": 256*1024*204,
                },
                # norandommap
                {
                    "norandommap": 1,
                    "zonerange": 4096,
                    "zonesize": 4096,
                    "bs": 4096,
                    "offset": 8*4096,
                    "size": 16*4096,
                    "io_size": 16*4096,
                },
                {
                    "norandommap": 1,
                    "zonerange": 4096,
                    "zonesize": 4096,
                    "bs": 4096,
                    "size": 16*4096,
                    "io_size": 16*4096,
                },
                {
                    "norandommap": 1,
                    "zonerange": 16*1024*1024,
                    "zonesize": 16*1024*1024,
                    "bs": 4096,
                    "size": 256*1024*1024,
                    "io_size": 256*1024*204,
                },
                {
                    "norandommap": 1,
                    "zonerange": 4096,
                    "zonesize": 8192,
                    "bs": 4096,
                    "size": 16*4096,
                    "io_size": 16*4096,
                },
                {
                    "norandommap": 1,
                    "zonerange": 16*1024*1024,
                    "zonesize": 32*1024*1024,
                    "bs": 4096,
                    "size": 256*1024*1024,
                    "io_size": 256*1024*204,
                },
                {
                    "norandommap": 1,
                    "zonerange": 8192,
                    "zonesize": 4096,
                    "bs": 4096,
                    "size": 16*4096,
                    "io_size": 16*4096,
                },
                {
                    "norandommap": 1,
                    "zonerange": 16*1024*1024,
                    "zonesize": 8*1024*1024,
                    "bs": 4096,
                    "size": 256*1024*1024,
                    "io_size": 256*1024*1024,
                },

            ]

    index = 1
    passed = 0
    failed = 0

    if args.filename:
        statinfo = os.stat(args.filename)
        filesize = statinfo.st_size
        if filesize == 0:
            f = os.open(args.filename, os.O_RDONLY)
            filesize = os.lseek(f, 0, os.SEEK_END)
            os.close(f)

    for test in tests:
        if args.filename:
            test['filename'] = args.filename
            test['filesize'] = filesize
        else:
            test['filesize'] = test['size']
        iops_log = run_fio(args.fio, test, index)
        status = check_output(iops_log, test)
        print("Test {0} {1}".format(index, ("PASSED" if status else "FAILED")))
        if status:
            passed = passed + 1
        else:
            failed = failed + 1
        index = index + 1

    print("{0} tests passed, {1} failed".format(passed, failed))

    sys.exit(failed)
