diff --git a/.travis.yml b/.travis.yml index bb6314e..7ae0c62 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,5 +5,5 @@ python: - "3.6" - "pypy" - "pypy3" -script: python unit_test.py - +install: pip install tox-travis +script: tox diff --git a/cyaron/__init__.py b/cyaron/__init__.py index 6fdb07d..c9b81a1 100644 --- a/cyaron/__init__.py +++ b/cyaron/__init__.py @@ -17,4 +17,5 @@ from .math import * from .merger import Merger #from .visual import visualize +from . import log from random import randint, randrange, uniform, choice, random diff --git a/cyaron/compare.py b/cyaron/compare.py index cf3991c..fbe0ef2 100644 --- a/cyaron/compare.py +++ b/cyaron/compare.py @@ -1,39 +1,31 @@ -from __future__ import absolute_import -from cyaron import IO +from __future__ import absolute_import, print_function +from cyaron import IO, log from cyaron.utils import * from cyaron.consts import * from cyaron.graders import CYaRonGraders import subprocess +import multiprocessing import sys from io import open +import os + + +class CompareMismatch(ValueError): + def __init__(self, name, mismatch): + super(CompareMismatch, self).__init__(name, mismatch) + self.name = name + self.mismatch = mismatch class Compare: @staticmethod - def __compare_two(name, content, std, grader, **kwargs): + def __compare_two(name, content, std, grader): (result, info) = CYaRonGraders.invoke(grader, content, std) - - info = info if info is not None else "" status = "Correct" if result else "!!!INCORRECT!!!" - print("%s: %s %s" % (name, status, info)) - - stop_on_incorrect = kwargs.get("stop_on_incorrect", False) - custom_dump_data = kwargs.get("dump_data", None) - if stop_on_incorrect and not result: - if custom_dump_data: - (dump_name, dump_lambda) = custom_dump_data - with open(dump_name, "w", newline='\n') as f: - f.write(dump_lambda()) - - with open("std.out", "w", newline='\n') as f: - f.write(std) - with open("%s.out" % name, "w", newline='\n') as f: - f.write(content) - - print("Relevant files dumped.") - - sys.exit(0) - + info = info if info is not None else "" + log.debug("{}: {} {}".format(name, status, info)) + if not result: + raise CompareMismatch(name, info) @staticmethod def __process_file(file): @@ -46,53 +38,97 @@ def __process_file(file): return file, f.read() @staticmethod - def output(*args, **kwargs): - if len(args) == 0: - raise Exception("You must specify some files to compare.") - - if "std" not in kwargs: - raise Exception("You must specify a std.") - (_, std) = Compare.__process_file(kwargs["std"]) - - grader = kwargs.get("grader", DEFAULT_GRADER) - stop_on_incorrect = kwargs.get("stop_on_incorrect", False) + def __normal_max_workers(workers): + if workers is None: + if sys.version_info < (3, 5): + cpu = multiprocessing.cpu_count() + return cpu * 5 if cpu is not None else 1 + return workers + + @classmethod + def output(cls, *files, **kwargs): + kwargs = unpack_kwargs('output', kwargs, ('std', ('grader', DEFAULT_GRADER), ('max_workers', -1), ('job_pool', None))) + std = kwargs['std'] + grader = kwargs['grader'] + max_workers = kwargs['max_workers'] + job_pool = kwargs['job_pool'] + if (max_workers is None or max_workers >= 0) and job_pool is None: + max_workers = cls.__normal_max_workers(max_workers) + try: + from concurrent.futures import ThreadPoolExecutor + with ThreadPoolExecutor(max_workers=max_workers) as job_pool: + return cls.output(*files, std=std, grader=grader, max_workers=max_workers, job_pool=job_pool) + except ImportError: + pass + + def get_std(): + return cls.__process_file(std)[1] + if job_pool is not None: + std = job_pool.submit(get_std).result() + else: + std = get_std() - for file in args: - (file_name, content) = Compare.__process_file(file) - Compare.__compare_two(file_name, content, std, grader, stop_on_incorrect=stop_on_incorrect) + def do(file): + (file_name, content) = cls.__process_file(file) + cls.__compare_two(file_name, content, std, grader) - @staticmethod - def program(*args, **kwargs): - if len(args) == 0: - raise Exception("You must specify some programs to compare.") + if job_pool is not None: + job_pool.map(do, files) + else: + [x for x in map(do, files)] - if "input" not in kwargs: - raise Exception("You must specify an input.") + @classmethod + def program(cls, *programs, **kwargs): + kwargs = unpack_kwargs('program', kwargs, ('input', ('std', None), ('std_program', None), ('grader', DEFAULT_GRADER), ('max_workers', -1), ('job_pool', None))) input = kwargs['input'] + std = kwargs['std'] + std_program = kwargs['std_program'] + grader = kwargs['grader'] + max_workers = kwargs['max_workers'] + job_pool = kwargs['job_pool'] + if (max_workers is None or max_workers >= 0) and job_pool is None: + max_workers = cls.__normal_max_workers(max_workers) + try: + from concurrent.futures import ThreadPoolExecutor + with ThreadPoolExecutor(max_workers=max_workers) as job_pool: + return cls.program(*programs, input=input, std=std, std_program=std_program, grader=grader, max_workers=max_workers, job_pool=job_pool) + except ImportError: + pass + if not isinstance(input, IO): - raise Exception("Input must be an IO instance.") + raise TypeError("expect {}, got {}".format(type(IO).__name__, type(input).__name__)) input.flush_buffer() input.input_file.seek(0) - std = None - if "std" not in kwargs and "std_program" not in kwargs: - raise Exception("You must specify a std or a std_program.") - else: - if "std_program" in kwargs: - std = make_unicode(subprocess.check_output(kwargs['std_program'], shell=True, stdin=input.input_file, universal_newlines=True)) + if std_program is not None: + def get_std(): + return make_unicode(subprocess.check_output(std_program, shell=(not list_like(std_program)), stdin=input.input_file, universal_newlines=True)) + if job_pool is not None: + std = job_pool.submit(get_std).result() else: - (_, std) = Compare.__process_file(kwargs["std"]) - - grader = kwargs.get("grader", DEFAULT_GRADER) - stop_on_incorrect = kwargs.get("stop_on_incorrect", False) - - for program_name in args: - input.input_file.seek(0) - content = make_unicode(subprocess.check_output(program_name, shell=True, stdin=input.input_file, universal_newlines=True)) - - input.input_file.seek(0) - Compare.__compare_two(program_name, content, std, grader, - stop_on_incorrect=stop_on_incorrect, - dump_data=("error_input.in", lambda: input.input_file.read())) # Lazy dump - - input.input_file.seek(0, 2) + std = get_std() + elif std is not None: + def get_std(): + return cls.__process_file(std)[1] + if job_pool is not None: + std = job_pool.submit(get_std).result() + else: + std = get_std() + else: + raise TypeError('program() missing 1 required non-None keyword-only argument: \'std\' or \'std_program\'') + + def do(program_name): + timeout = None + if list_like(program_name) and len(program_name) == 2 and int_like(program_name[-1]): + program_name, timeout = program_name + with open(os.dup(input.input_file.fileno()), 'r', newline='\n') as input_file: + if timeout is None: + content = make_unicode(subprocess.check_output(program_name, shell=(not list_like(program_name)), stdin=input_file, universal_newlines=True)) + else: + content = make_unicode(subprocess.check_output(program_name, shell=(not list_like(program_name)), stdin=input_file, universal_newlines=True, timeout=timeout)) + cls.__compare_two(program_name, content, std, grader) + + if job_pool is not None: + job_pool.map(do, programs) + else: + [x for x in map(do, programs)] diff --git a/cyaron/graders/fulltext.py b/cyaron/graders/fulltext.py index 5b5260a..8460b6f 100644 --- a/cyaron/graders/fulltext.py +++ b/cyaron/graders/fulltext.py @@ -1,9 +1,10 @@ import hashlib from .graderregistry import CYaRonGraders +from .mismatch import HashMismatch @CYaRonGraders.grader("FullText") def fulltext(content, std): content_hash = hashlib.sha256(content.encode('utf-8')).hexdigest() std_hash = hashlib.sha256(std.encode('utf-8')).hexdigest() - return (True, None) if content_hash == std_hash else (False, "Hash mismatch: read %s, expected %s" % (content_hash, std_hash)) + return (True, None) if content_hash == std_hash else (False, HashMismatch(content, std, content_hash, std_hash)) diff --git a/cyaron/graders/mismatch.py b/cyaron/graders/mismatch.py new file mode 100644 index 0000000..70c2dfc --- /dev/null +++ b/cyaron/graders/mismatch.py @@ -0,0 +1,48 @@ +class Mismatch(ValueError): + """exception for content mismatch""" + def __init__(self, content, std, *args): + """ + content -> content got + std -> content expected + """ + super(Mismatch, self).__init__(content, std, *args) + self.content = content + self.std = std + +class HashMismatch(Mismatch): + """exception for hash mismatch""" + def __init__(self, content, std, content_hash, std_hash): + """ + content -> content got + std -> content expected + content_hash -> hash of content + std_hash -> hash of std + """ + super(HashMismatch, self).__init__(content, std, content_hash, std_hash) + self.content_hash = content_hash + self.std_hash = std_hash + + def __str__(self): + return "Hash mismatch: read %s, expected %s" % (self.content_hash, self.std_hash) + +class TextMismatch(Mismatch): + """exception for text mismatch""" + def __init__(self, content, std, err_msg, lineno=None, colno=None, content_token=None, std_token=None): + """ + content -> content got + std -> content expected + err_msg -> error message template like "wrong on line {} col {} read {} expected {}" + lineno -> line number + colno -> column number + content_token -> the token of content mismatch + std_token -> the token of std + """ + super(TextMismatch, self).__init__(content, std, err_msg, lineno, colno, content_token, std_token) + self.err_msg = err_msg.format(lineno, colno, content_token, std_token) + self.lineno = lineno + self.colno = colno + self.content_token = content_token + self.std_token = std_token + + def __str__(self): + return self.err_msg diff --git a/cyaron/graders/noipstyle.py b/cyaron/graders/noipstyle.py index 3895d53..9af2d9b 100644 --- a/cyaron/graders/noipstyle.py +++ b/cyaron/graders/noipstyle.py @@ -1,5 +1,6 @@ from ..utils import * from .graderregistry import CYaRonGraders +from .mismatch import TextMismatch @CYaRonGraders.grader("NOIPStyle") @@ -7,17 +8,17 @@ def noipstyle(content, std): content_lines = strtolines(content.replace('\r\n', '\n')) std_lines = strtolines(std.replace('\r\n', '\n')) if len(content_lines) != len(std_lines): - return False, 'Too many or too few lines.' + return False, TextMismatch(content, std, 'Too many or too few lines.') for i in range(len(content_lines)): if std_lines[i] != content_lines[i]: for j in range(min(len(std_lines[i]), len(content_lines[i]))): if std_lines[i][j] != content_lines[i][j]: - return (False, 'On line %d column %d, read %s, expected %s.' - % (i + 1, j + 1, content_lines[i][j:j + 5], std_lines[i][j:j + 5])) + return (False, TextMismatch(content, std, 'On line {} column {}, read {}, expected {}.', + i + 1, j + 1, content_lines[i][j:j + 5], std_lines[i][j:j + 5])) if len(std_lines[i]) > len(content_lines[i]): - return False, 'Too short on line %d.' % i + return False, TextMismatch(content, std, 'Too short on line {}.', i) if len(std_lines[i]) < len(content_lines[i]): - return False, 'Too long on line %d.' % i + return False, TextMismatch(content, std, 'Too long on line {}.', i) - return True, None \ No newline at end of file + return True, None diff --git a/cyaron/io.py b/cyaron/io.py index fe51af6..83497fc 100644 --- a/cyaron/io.py +++ b/cyaron/io.py @@ -1,21 +1,28 @@ from __future__ import absolute_import from .utils import * -from io import open +from . import log +from io import open, IOBase import subprocess import tempfile import os +import re class IO(object): """Class IO: IO tool class. It will process the input and output files.""" - def __init__(self, *args, **kwargs): - """__init__(self, *args, **kwargs) -> None - (str,str) args -> The file names of input file and output file. Index 0 is the name of input file, and index 1 is for output file - **kwargs: + def __init__(self, input_file=None, output_file=None, data_id=None, file_prefix=None, input_suffix='.in', output_suffix='.out', disable_output=False): + """__init__(self, input_file=None, output_file=None, data_id=None, file_prefix=None, input_suffix='.in', output_suffix='.out', disable_output=False) -> None + input_file, output_file overload: + None -> make a temp file (if file_prefix is None) + file object -> treat the file-like object as in/output file + int -> open file by file descriptor + str -> a filename or filename template like 'awd{}.in'. ``{}`` will be replaced by ``data_id`` + int data_id -> the id of the data. if it's None, the file names will not contain the id. + legacy argumants: str file_prefix -> the prefix for the input and output files - int data_id -> the id of the data. if it's None, the file names will not contain the id. str input_suffix = ".in" -> the suffix of the input file str output_suffix = ".out" -> the suffix of the output file + disable_output -> bool, set to True to disable output Examples: IO("a","b") -> create input file "a" and output file "b" IO("a.in","b.out") -> create input file "a.in" and output file "b.out" @@ -24,74 +31,95 @@ def __init__(self, *args, **kwargs): IO(file_prefix="data",input_suffix=".input") -> create input file "data.input" and output file "data.out" IO(file_prefix="data",output_suffix=".output") -> create input file "data.in" and output file "data.output" IO(file_prefix="data",data_id=2,input_suffix=".input") -> create input file "data2.input" and output file "data2.out" + IO("data{}.in","data{}.out",data_id=2) -> create input file "data2.in" and output file "data2.out" + IO(open('data.in', 'w+'), open('data.out', 'w+')) -> input file "data.in" and output file "data.out" """ - if len(args) == 0: - if not "file_prefix" in kwargs: - self.file_flag = 0 - (fd, self.input_filename) = tempfile.mkstemp() - os.close(fd) - (fd, self.output_filename) = tempfile.mkstemp() - os.close(fd) + if file_prefix is not None: + # legacy mode + input_file = '{}{{}}{}'.format(self.__escape_format(file_prefix), self.__escape_format(input_suffix)) + output_file = '{}{{}}{}'.format(self.__escape_format(file_prefix), self.__escape_format(output_suffix)) + self.input_filename, self.output_filename = None, None + self.__input_temp, self.__output_temp = False, False + self.__init_file(input_file, data_id, 'i') + if not disable_output: + self.__init_file(output_file, data_id, 'o') + else: + self.output_file = None + self.__closed = False + self.is_first_char = {} + + def __init_file(self, f, data_id, file_type): + try: + is_file = isinstance(f, file) + except NameError: + is_file = False + if isinstance(f, IOBase) or is_file: + # consider ``f`` as a file object + if file_type == 'i': + self.input_file = f else: - self.file_flag = 2 - if "data_id" in kwargs: - filename_prefix = "%s%d" % (kwargs["file_prefix"], kwargs["data_id"]) - else: - filename_prefix = kwargs["file_prefix"] - - input_suffix = kwargs.get("input_suffix", ".in") - output_suffix = kwargs.get("output_suffix", ".out") - disable_output = kwargs.get("disable_output", False) - self.input_filename = filename_prefix + input_suffix - self.output_filename = filename_prefix + output_suffix if not disable_output else None - elif len(args) == 1: - self.file_flag = 1 - self.input_filename = args[0] - (fd, self.output_filename) = tempfile.mkstemp() - os.close(fd) - elif len(args) == 2: - self.file_flag = 2 - self.input_filename = args[0] - self.output_filename = args[1] + self.output_file = f + elif isinstance(f, int): + # consider ``f`` as a file descor + self.__init_file(open(f, 'w+', newline='\n'), data_id, file_type) + elif f is None: + # consider wanna temp file + fd, self.input_filename = tempfile.mkstemp() + self.__init_file(fd, data_id, file_type) + if file_type == 'i': + self.__input_temp = True + else: + self.__output_temp = True else: - raise Exception("Invalid argument count") + # consider ``f`` as filename template + filename = f.format(data_id) + if file_type == 'i': + self.input_filename = filename + log.debug("Processing %s" % self.input_filename) + else: + self.output_filename = filename + self.__init_file(open(filename, 'w+', newline='\n'), data_id, file_type) - self.input_file = open(self.input_filename, 'w+', newline='\n') - self.output_file = open(self.output_filename, 'w+', newline='\n') if self.output_filename else None - self.is_first_char = dict() - if self.file_flag != 0: - print("Processing %s" % self.input_filename) + def __escape_format(self, st): + """replace "{}" to "{{}}" """ + return re.sub(r'\{', '{{', re.sub(r'\}', '}}', st)) - def __del__(self): - """__del__(self) -> None - Delete the IO object and close the input file and the output file - """ + def __del_files(self): + """delete files""" + if self.__input_temp and self.input_filename is not None: + os.remove(self.input_filename) + if self.__output_temp and self.output_filename is not None: + os.remove(self.output_filename) + + def close(self): + """Delete the IO object and close the input file and the output file""" + if self.__closed: + # avoid double close + return + deleted = False try: - self.input_file.close() - self.output_file.close() - if self.file_flag <= 1: - os.remove(self.output_filename) - if self.file_flag == 0: - os.remove(self.input_filename) - except Exception: + # on posix, one can remove a file while it's opend by a process + # the file then will be not visable to others, but process still have the file descriptor + # it is recommand to remove temp file before close it on posix to avoid race + # on nt, it will just fail and raise OSError so that after closing remove it again + self.__del_files() + deleted = True + except OSError: pass + self.input_file.close() + self.output_file.close() + if not deleted: + self.__del_files() + self.__closed = True + + def __del__(self): + self.close() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): - """__del__(self) -> None - Exit the context of the IO object and close the input file and the output file - """ - try: - self.input_file.close() - self.output_file.close() - if self.file_flag <= 1: - os.remove(self.output_filename) - if self.file_flag == 0: - os.remove(self.input_filename) - except Exception: - pass + self.close() def __write(self, file, *args, **kwargs): """__write(self, file, *args, **kwargs) -> None @@ -135,12 +163,13 @@ def output_gen(self, shell_cmd): Run the command shell_cmd(usually the std programme) and send it the input file as stdin. Write its output to the output file. str shell_cmd -> the command to run, usually the std programme """ - self.input_file.close() - with open(self.input_filename, 'r') as f: - self.output_file.write(make_unicode(subprocess.check_output(shell_cmd, shell=True, stdin=f, universal_newlines=True))) + self.flush_buffer() + origin_pos = self.input_file.tell() + self.input_file.seek(0) + subprocess.check_call(shell_cmd, shell=True, stdin=self.input_file, stdout=self.output_file, universal_newlines=True) + self.input_file.seek(origin_pos) - self.input_file = open(self.input_filename, 'a+') - print(self.output_filename, " done") + log.debug(self.output_filename, " done") def output_write(self, *args, **kwargs): """output_write(self, *args, **kwargs) -> None diff --git a/cyaron/log.py b/cyaron/log.py new file mode 100644 index 0000000..33b3b0c --- /dev/null +++ b/cyaron/log.py @@ -0,0 +1,102 @@ +from __future__ import print_function +from functools import partial +import sys +from threading import Lock +try: + import colorful +except ImportError: + class colorful: + def __getattr__(self, attr): + return lambda st: st + colorful = colorful() +from .utils import make_unicode + +__print = print +def _print(*args, **kwargs): + flush = False + if 'flush' in kwargs: + flush = kwargs['flush'] + del kwargs['flush'] + __print(*args, **kwargs) + if flush: + kwargs.get('file', sys.stdout).flush() + +def _join_dict(a, b): + """join two dict""" + c = a.copy() + for k, v in b.items(): + c[k] = v + return c + +_log_funcs = {} +_log_lock = Lock() +def log(funcname, *args, **kwargs): + """log with log function specified by ``funcname``""" + _log_lock.acquire() + rv = _log_funcs.get(funcname, lambda *args, **kwargs: None)(*args, **kwargs) + _log_lock.release() + return rv + +"""5 log levels +1. debug: debug info +2. info: common info +3. print: print output +4. warn: warnings +5. error: errors +""" + +debug = partial(log, 'debug') +info = partial(log, 'info') +print = partial(log, 'print') +warn = partial(log, 'warn') +error = partial(log, 'error') + +def register_logfunc(funcname, func): + """register logfunc + str funcname -> name of logfunc + callable func -> logfunc + """ + if func is not None: + _log_funcs[funcname] = func + else: + try: + del _log_funcs[funcname] + except KeyError: + pass + +_nb_print = lambda *args, **kwargs: _print(*args, **_join_dict(kwargs, {'flush': True})) +_nb_print_e = lambda *args, **kwargs: _print(*args, **_join_dict(kwargs, {'file': sys.stderr, 'flush': True})) +_cl_print = lambda color, *args, **kwargs: _nb_print(*[color(make_unicode(item)) for item in args], **kwargs) if sys.stdout.isatty() else _nb_print(*args, **kwargs) +_cl_print_e = lambda color, *args, **kwargs: _nb_print_e(*[color(make_unicode(item)) for item in args], **kwargs) if sys.stderr.isatty() else _nb_print_e(*args, **kwargs) + +_default_debug = partial(_cl_print, colorful.cyan) +_default_info = partial(_cl_print, colorful.blue) +_default_print = _nb_print +_default_warn = partial(_cl_print_e, colorful.yellow) +_default_error = partial(_cl_print_e, colorful.red) + +def set_quiet(): + """set log mode to "quiet" """ + register_logfunc('debug', None) + register_logfunc('info', None) + register_logfunc('print', _default_print) + register_logfunc('warn', None) + register_logfunc('error', _default_error) + +def set_normal(): + """set log mode to "normal" """ + register_logfunc('debug', None) + register_logfunc('info', _default_info) + register_logfunc('print', _default_print) + register_logfunc('warn', _default_warn) + register_logfunc('error', _default_error) + +def set_verbose(): + """set log mode to "verbose" """ + register_logfunc('debug', _default_debug) + register_logfunc('info', _default_info) + register_logfunc('print', _default_print) + register_logfunc('warn', _default_warn) + register_logfunc('error', _default_error) + +set_normal() diff --git a/cyaron/math.py b/cyaron/math.py index 9a21471..210ff62 100644 --- a/cyaron/math.py +++ b/cyaron/math.py @@ -4,6 +4,7 @@ ''' from __future__ import absolute_import from math import sqrt, ceil +from fractions import gcd from functools import reduce import random import itertools @@ -176,25 +177,6 @@ def factor(n): factors.append((f, e)) -#--- greatest common divisor---------------------------------------------------------------------- -def gcd(a, b): - """ - Compute the greatest common divisor of a and b. Examples: - - >>> gcd(14, 15) #co-prime - 1 - >>> gcd(5*5, 3*5) - 5 - """ - if a < 0: a = -a - if b < 0: b = -b - if a == 0: return b - while (b): a, b = b, a%b - return a - - - - #--- generate permutations----------------------------------------------------------------------- def perm(n, s): """ diff --git a/cyaron/tests/compare_test.py b/cyaron/tests/compare_test.py index 1cc7c1e..1046b7c 100644 --- a/cyaron/tests/compare_test.py +++ b/cyaron/tests/compare_test.py @@ -1,9 +1,15 @@ import unittest import os +import sys import shutil import tempfile -from cyaron import IO, Compare +import subprocess +from cyaron import IO, Compare, log from cyaron.output_capture import captured_output +from cyaron.graders.mismatch import * +from cyaron.compare import CompareMismatch + +log.set_verbose() class TestCompare(unittest.TestCase): @@ -41,8 +47,17 @@ def test_noipstyle_incorrect(self): with open("test_another_incorrect.out", "w") as f: f.write("test123\r\ntest124 ") - with captured_output() as (out, err): - Compare.output("test_another_incorrect.out", std=io) + try: + with captured_output() as (out, err): + Compare.output("test_another_incorrect.out", std=io) + except CompareMismatch as e: + self.assertEqual(e.name, 'test_another_incorrect.out') + e = e.mismatch + self.assertEqual(e.content, 'test123\r\ntest124 ') + self.assertEqual(e.std, 'test123 \ntest123\n\n') + self.assertEqual(str(e), 'On line 2 column 7, read 4, expected 3.') + else: + self.assertTrue(False) result = out.getvalue().strip() self.assertEqual(result, "test_another_incorrect.out: !!!INCORRECT!!! On line 2 column 7, read 4, expected 3.") @@ -60,10 +75,50 @@ def test_fulltext_program(self): io.output_writeln("1") - with captured_output() as (out, err): - Compare.program("python correct.py", "python incorrect.py", std=io, input=io, grader="FullText") + try: + with captured_output() as (out, err): + Compare.program("python correct.py", "python incorrect.py", std=io, input=io, grader="FullText") + except CompareMismatch as e: + self.assertEqual(e.name, 'python incorrect.py') + e = e.mismatch + self.assertEqual(e.content, '2\n') + self.assertEqual(e.std, '1\n') + self.assertEqual(e.content_hash, '53c234e5e8472b6ac51c1ae1cab3fe06fad053beb8ebfd8977b010655bfdd3c3') + self.assertEqual(e.std_hash, '4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865') + else: + self.assertTrue(False) result = out.getvalue().strip() - correct_text = 'python correct.py: Correct \npython incorrect.py: !!!INCORRECT!!! Hash mismatch: read 53c234e5e8472b6ac51c1ae1cab3fe06fad053beb8ebfd8977b010655bfdd3c3, expected 4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865' - self.assertEqual(result, correct_text) - + correct_out = 'python correct.py: Correct \npython incorrect.py: !!!INCORRECT!!! Hash mismatch: read 53c234e5e8472b6ac51c1ae1cab3fe06fad053beb8ebfd8977b010655bfdd3c3, expected 4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865' + self.assertEqual(result, correct_out) + + def test_concurrent(self): + programs = ['test{}.py'.format(i) for i in range(16)] + for fn in programs: + with open(fn, 'w') as f: + f.write('print({})'.format(16)) + with open('std.py', 'w') as f: + f.write('print({})'.format(16)) + with IO() as test: + Compare.program(*[(sys.executable, program) for program in programs], std_program=(sys.executable, 'std.py'), max_workers=None, input=test) + + ios = [IO() for i in range(16)] + try: + for f in ios: + f.output_write('16') + with IO() as std: + std.output_write('16') + Compare.output(*ios, std=std, max_workers=None) + finally: + for io in ios: + io.close() + + def test_timeout(self): + if sys.version_info >= (3, 3): + with IO() as test: + try: + Compare.program(((sys.executable, '-c', '__import__(\'time\').sleep(10)'), 1), std=test, input=test) + except subprocess.TimeoutExpired: + pass + else: + self.assertTrue(False) diff --git a/cyaron/tests/io_test.py b/cyaron/tests/io_test.py index 0ef2075..02b10c4 100644 --- a/cyaron/tests/io_test.py +++ b/cyaron/tests/io_test.py @@ -56,4 +56,17 @@ def test_output_gen(self): with open("test_gen.out") as f: output = f.read() - self.assertEqual(output.strip("\n"), "233") \ No newline at end of file + self.assertEqual(output.strip("\n"), "233") + + def test_init_overload(self): + with IO(file_prefix='data{', data_id=5) as test: + self.assertEqual(test.input_filename, 'data{5.in') + self.assertEqual(test.output_filename, 'data{5.out') + with IO('data{}.in', 'data{}.out', 5) as test: + self.assertEqual(test.input_filename, 'data5.in') + self.assertEqual(test.output_filename, 'data5.out') + with open('data5.in', 'w+') as fin: + with open('data5.out', 'w+') as fout: + with IO(fin, fout) as test: + self.assertEqual(test.input_file, fin) + self.assertEqual(test.output_file, fout) diff --git a/cyaron/utils.py b/cyaron/utils.py index eaf61df..fc4e49b 100644 --- a/cyaron/utils.py +++ b/cyaron/utils.py @@ -13,6 +13,16 @@ def list_like(data): return isinstance(data, tuple) or isinstance(data, list) +def int_like(data): + isint = False + try: + isint = isint or isinstance(date, long) + except NameError: + pass + isint = isint or isinstance(data, int) + return isint + + def strtolines(str): lines = str.split('\n') for i in range(len(lines)): @@ -27,4 +37,28 @@ def make_unicode(data): try: return unicode(data) except NameError: - return str(data) \ No newline at end of file + return str(data) + +def unpack_kwargs(funcname, kwargs, arg_pattern): + rv = {} + kwargs = kwargs.copy() + for tp in arg_pattern: + if list_like(tp): + k, v = tp + rv[k] = kwargs.get(k, v) + try: + del kwargs[k] + except KeyError: + pass + else: + error = False + try: + rv[tp] = kwargs[tp] + del kwargs[tp] + except KeyError as e: + error = True + if error: + raise TypeError('{}() missing 1 required keyword-only argument: \'{}\''.format(funcname, tp)) + if kwargs: + raise TypeError('{}() got an unexpected keyword argument \'{}\''.format(funcname, next(iter(kwargs.items()))[0])) + return rv diff --git a/setup.py b/setup.py index 0cac8c4..dabeecc 100644 --- a/setup.py +++ b/setup.py @@ -12,5 +12,5 @@ packages=find_packages(), include_package_data=True, platforms='any', - install_requires=[], + install_requires=['colorful>=0.3.5'], ) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..ed1e45d --- /dev/null +++ b/tox.ini @@ -0,0 +1,2 @@ +[testenv] +commands=python unit_test.py