#!/usr/bin/env python3


import argparse, textwrap, os, sys, re, random, lzma
from pathlib import Path, PurePath


### Constants:
os.environ["LIBCPUID_NO_WARN"] = "1"
delimiter = "-" * 80
fields_x86 = [ "architecture", "feature-level", "purpose", "family", "model", "stepping",
	   "extfamily", "extmodel", "cores", "logical",
	   "l1d-cache",     "l1i-cache",     "l2-cache",     "l3-cache",     "l4-cache",
	   "l1d-assoc",     "l1i-assoc",     "l2-assoc",     "l3-assoc",     "l4-assoc",
	   "l1d-cacheline", "l1i-cacheline", "l2-cacheline", "l3-cacheline", "l4-cacheline",
	   "l1d-instances", "l1i-instances", "l2-instances", "l3-instances", "l4-instances",
	   "sse-size", "codename", "technology", "flags" ]
fields_arm = [ "architecture", "feature-level", "purpose",
	   "implementer", "variant", "part-num", "revision",
	   "cores", "logical",
	   "codename", "technology", "flags" ]

# One would usually use os.tempnam, but libc gives off hell a lot of
# warnings when you attempt to use that :(
def make_tempname(prefix):
	chars = ""
	for i in range(26):
		chars += chr(97+i)
		chars += chr(65+i)
	for i in range(10):
		chars += chr(48+i)
	for i in range(6):
		prefix += random.choice(chars)
	return prefix

def fmt_error(err):
	pfix = f"  {err[0]}: "
	return "{} expected `{}'\n{} got      `{}'".format(pfix, err[1], ' '*len(pfix), err[2])

def fixFile(filename, input_lines, output_lines):
	pfilename = Path(filename)
	if pfilename.suffixes[-1] == ".xz":
		# XZ compressed file
		f = lzma.open(pfilename, "wt")
	else:
		# Plain text file
		f = open(pfilename, "wt")
	f.writelines([s + "\n" for s in input_lines])
	f.write(delimiter + "\n")
	f.writelines([s + "\n" for s in output_lines])
	f.close()

def do_test(inp, expected_out, binary, test_file_name, num_cpu_type):
	fninp = make_tempname("cpuidin")
	fnoutp = make_tempname("cpuidout")
	f = open(fninp, "wt")
	f.writelines([s + "\n" for s in inp])
	f.close()
	architecture = os.popen(f"{binary} --load={fninp} --architecture").read().splitlines()[-1]
	if architecture == "x86":
		fields = fields_x86
	elif architecture == "ARM":
		fields = fields_arm
	else:
		fields = []
	cmd = f"{binary} --load={fninp} --outfile={fnoutp} {' '.join(['--' + s for s in fields])}"
	os.system(cmd)
	os.unlink(fninp)
	real_out = []
	real_out_delim = [] # when fixing the file, delimiters are required
	try:
		f = open(fnoutp, "rt")
		for s in f.readlines():
			real_out_delim.append(s.strip())
			if delimiter not in s:
				real_out.append(s.strip())
		f.close()
		os.unlink(fnoutp)
	except IOError:
		return "Exception"
	if len(real_out) != len(expected_out) or len(real_out) != len(fields) * num_cpu_type:
		if args.fix:
			fixFile(test_file_name, inp, real_out_delim)
			return "Number of records, fixed."
		else:
			return "Unexpected number of records returned\n - expected length {}\n - real length {}\n - {} fields".format(len(expected_out), len(real_out), len(fields) * num_cpu_type)
	err_fields = []
	for i in range(len(real_out)):
		if real_out[i] != expected_out[i]:
			err_fields.append((fields[i % len(fields)], expected_out[i], real_out[i]))
	if not err_fields:
		return "OK"
	else:
		if args.fix:
			fixFile(test_file_name, inp, real_out_delim)
			return "Mismatch, fixed."
		else:
			return "Mismatch in fields:\n{}".format('\n'.join([fmt_error(err) for err in err_fields]))

def is_regular_file(filename):
	try:
		with open(filename, 'r') as fd:
			fd.read()
			return True
	except:
		return False

def check_type_binary_file(filename):
	if not Path(filename).is_file():
		raise argparse.ArgumentTypeError(f"{filename} is not a file")
	if is_regular_file(filename):
		raise argparse.ArgumentTypeError(f"{filename} is not a binary file")
	return filename

# Parse arguments
parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
	description=textwrap.dedent("""

Run test files.

If a test file is given, it is tested by itself.
If a directory is given, process all *.test files there, subdirectories included.

If the --fix option is given, the behaviour of the cpuid_tool binary is deemed correct and any failing tests are updated.
"""))
parser.add_argument("cpuid_tool",
	nargs='?',
	type=check_type_binary_file,
	default="./build/cpuid_tool/cpuid_tool",
	help="path to the cpuid_tool binary")
parser.add_argument("input_test_files",
	nargs='+',
	default=["./tests"],
	help="test file or directory containing test files")
parser.add_argument("--fix",
	action=argparse.BooleanOptionalAction,
	default=False,
	help="update failing tests (default is false)")
parser.add_argument("--show-test-fast-warning",
	dest="show_test_fast_warning",
	action=argparse.BooleanOptionalAction,
	default=False,
	help="show a warning on errors (default if false)")
args = parser.parse_args()

# Create test files list
filelist = []
for input_test_file in args.input_test_files:
	if Path(input_test_file).is_dir():
		# gather all *.test files from subdirs amd, intel and cie:
		for dirpath, dirnames, filenames in os.walk(input_test_file):
			filelist += [PurePath(dirpath).joinpath(fn) for fn in filenames if ".test" in Path(fn).suffixes]
	else:
		filelist.append(input_test_file)

# Run tests
errors = False
print("Testing...")
for test_file_name_raw in filelist:
	test_file_name = Path(test_file_name_raw)
	num_cpu_type = 0
	current_input = []
	current_output = []
	build_output = False
	if test_file_name.suffixes[-1] == ".xz":
		# XZ compressed file
		f = lzma.open(test_file_name, "rt")
	else:
		# Plain text file
		f = open(test_file_name, "rt")
	# Read file line by line
	for line in f.readlines():
		line = line.strip()
		if line == delimiter:
			build_output = True
			num_cpu_type += 1
		else:
			if build_output:
				current_output.append(line)
			else:
				current_input.append(line)
	f.close()
	#codename = current_output[len(current_output) - 2]
	result = do_test(current_input, current_output, args.cpuid_tool, test_file_name, num_cpu_type)
	print(f"Test [{test_file_name.name}]: {result}")
	if result != "OK":
		errors = True
	build_output = False

if errors:
	if args.show_test_fast_warning:
		print("""
You're running tests in fast mode; before taking any action on the errors
above, please confirm that the slow mode ('make test-old') also fails.""")
	sys.exit(1)
else:
	print("All successfull!")
	sys.exit(0)
