Skip to content

Commit 656af22

Browse files
authored
Merge pull request #25 from TitanSnow/TitanSnow
little improvements and bugs fixing
2 parents 454d6b9 + f9f7f89 commit 656af22

File tree

14 files changed

+475
-171
lines changed

14 files changed

+475
-171
lines changed

.travis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ python:
55
- "3.6"
66
- "pypy"
77
- "pypy3"
8-
script: python unit_test.py
9-
8+
install: pip install tox-travis
9+
script: tox

cyaron/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@
1717
from .math import *
1818
from .merger import Merger
1919
#from .visual import visualize
20+
from . import log
2021
from random import randint, randrange, uniform, choice, random

cyaron/compare.py

Lines changed: 101 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,31 @@
1-
from __future__ import absolute_import
2-
from cyaron import IO
1+
from __future__ import absolute_import, print_function
2+
from cyaron import IO, log
33
from cyaron.utils import *
44
from cyaron.consts import *
55
from cyaron.graders import CYaRonGraders
66
import subprocess
7+
import multiprocessing
78
import sys
89
from io import open
10+
import os
11+
12+
13+
class CompareMismatch(ValueError):
14+
def __init__(self, name, mismatch):
15+
super(CompareMismatch, self).__init__(name, mismatch)
16+
self.name = name
17+
self.mismatch = mismatch
918

1019

1120
class Compare:
1221
@staticmethod
13-
def __compare_two(name, content, std, grader, **kwargs):
22+
def __compare_two(name, content, std, grader):
1423
(result, info) = CYaRonGraders.invoke(grader, content, std)
15-
16-
info = info if info is not None else ""
1724
status = "Correct" if result else "!!!INCORRECT!!!"
18-
print("%s: %s %s" % (name, status, info))
19-
20-
stop_on_incorrect = kwargs.get("stop_on_incorrect", False)
21-
custom_dump_data = kwargs.get("dump_data", None)
22-
if stop_on_incorrect and not result:
23-
if custom_dump_data:
24-
(dump_name, dump_lambda) = custom_dump_data
25-
with open(dump_name, "w", newline='\n') as f:
26-
f.write(dump_lambda())
27-
28-
with open("std.out", "w", newline='\n') as f:
29-
f.write(std)
30-
with open("%s.out" % name, "w", newline='\n') as f:
31-
f.write(content)
32-
33-
print("Relevant files dumped.")
34-
35-
sys.exit(0)
36-
25+
info = info if info is not None else ""
26+
log.debug("{}: {} {}".format(name, status, info))
27+
if not result:
28+
raise CompareMismatch(name, info)
3729

3830
@staticmethod
3931
def __process_file(file):
@@ -46,53 +38,97 @@ def __process_file(file):
4638
return file, f.read()
4739

4840
@staticmethod
49-
def output(*args, **kwargs):
50-
if len(args) == 0:
51-
raise Exception("You must specify some files to compare.")
52-
53-
if "std" not in kwargs:
54-
raise Exception("You must specify a std.")
55-
(_, std) = Compare.__process_file(kwargs["std"])
56-
57-
grader = kwargs.get("grader", DEFAULT_GRADER)
58-
stop_on_incorrect = kwargs.get("stop_on_incorrect", False)
41+
def __normal_max_workers(workers):
42+
if workers is None:
43+
if sys.version_info < (3, 5):
44+
cpu = multiprocessing.cpu_count()
45+
return cpu * 5 if cpu is not None else 1
46+
return workers
47+
48+
@classmethod
49+
def output(cls, *files, **kwargs):
50+
kwargs = unpack_kwargs('output', kwargs, ('std', ('grader', DEFAULT_GRADER), ('max_workers', -1), ('job_pool', None)))
51+
std = kwargs['std']
52+
grader = kwargs['grader']
53+
max_workers = kwargs['max_workers']
54+
job_pool = kwargs['job_pool']
55+
if (max_workers is None or max_workers >= 0) and job_pool is None:
56+
max_workers = cls.__normal_max_workers(max_workers)
57+
try:
58+
from concurrent.futures import ThreadPoolExecutor
59+
with ThreadPoolExecutor(max_workers=max_workers) as job_pool:
60+
return cls.output(*files, std=std, grader=grader, max_workers=max_workers, job_pool=job_pool)
61+
except ImportError:
62+
pass
63+
64+
def get_std():
65+
return cls.__process_file(std)[1]
66+
if job_pool is not None:
67+
std = job_pool.submit(get_std).result()
68+
else:
69+
std = get_std()
5970

60-
for file in args:
61-
(file_name, content) = Compare.__process_file(file)
62-
Compare.__compare_two(file_name, content, std, grader, stop_on_incorrect=stop_on_incorrect)
71+
def do(file):
72+
(file_name, content) = cls.__process_file(file)
73+
cls.__compare_two(file_name, content, std, grader)
6374

64-
@staticmethod
65-
def program(*args, **kwargs):
66-
if len(args) == 0:
67-
raise Exception("You must specify some programs to compare.")
75+
if job_pool is not None:
76+
job_pool.map(do, files)
77+
else:
78+
[x for x in map(do, files)]
6879

69-
if "input" not in kwargs:
70-
raise Exception("You must specify an input.")
80+
@classmethod
81+
def program(cls, *programs, **kwargs):
82+
kwargs = unpack_kwargs('program', kwargs, ('input', ('std', None), ('std_program', None), ('grader', DEFAULT_GRADER), ('max_workers', -1), ('job_pool', None)))
7183
input = kwargs['input']
84+
std = kwargs['std']
85+
std_program = kwargs['std_program']
86+
grader = kwargs['grader']
87+
max_workers = kwargs['max_workers']
88+
job_pool = kwargs['job_pool']
89+
if (max_workers is None or max_workers >= 0) and job_pool is None:
90+
max_workers = cls.__normal_max_workers(max_workers)
91+
try:
92+
from concurrent.futures import ThreadPoolExecutor
93+
with ThreadPoolExecutor(max_workers=max_workers) as job_pool:
94+
return cls.program(*programs, input=input, std=std, std_program=std_program, grader=grader, max_workers=max_workers, job_pool=job_pool)
95+
except ImportError:
96+
pass
97+
7298
if not isinstance(input, IO):
73-
raise Exception("Input must be an IO instance.")
99+
raise TypeError("expect {}, got {}".format(type(IO).__name__, type(input).__name__))
74100
input.flush_buffer()
75101
input.input_file.seek(0)
76102

77-
std = None
78-
if "std" not in kwargs and "std_program" not in kwargs:
79-
raise Exception("You must specify a std or a std_program.")
80-
else:
81-
if "std_program" in kwargs:
82-
std = make_unicode(subprocess.check_output(kwargs['std_program'], shell=True, stdin=input.input_file, universal_newlines=True))
103+
if std_program is not None:
104+
def get_std():
105+
return make_unicode(subprocess.check_output(std_program, shell=(not list_like(std_program)), stdin=input.input_file, universal_newlines=True))
106+
if job_pool is not None:
107+
std = job_pool.submit(get_std).result()
83108
else:
84-
(_, std) = Compare.__process_file(kwargs["std"])
85-
86-
grader = kwargs.get("grader", DEFAULT_GRADER)
87-
stop_on_incorrect = kwargs.get("stop_on_incorrect", False)
88-
89-
for program_name in args:
90-
input.input_file.seek(0)
91-
content = make_unicode(subprocess.check_output(program_name, shell=True, stdin=input.input_file, universal_newlines=True))
92-
93-
input.input_file.seek(0)
94-
Compare.__compare_two(program_name, content, std, grader,
95-
stop_on_incorrect=stop_on_incorrect,
96-
dump_data=("error_input.in", lambda: input.input_file.read())) # Lazy dump
97-
98-
input.input_file.seek(0, 2)
109+
std = get_std()
110+
elif std is not None:
111+
def get_std():
112+
return cls.__process_file(std)[1]
113+
if job_pool is not None:
114+
std = job_pool.submit(get_std).result()
115+
else:
116+
std = get_std()
117+
else:
118+
raise TypeError('program() missing 1 required non-None keyword-only argument: \'std\' or \'std_program\'')
119+
120+
def do(program_name):
121+
timeout = None
122+
if list_like(program_name) and len(program_name) == 2 and int_like(program_name[-1]):
123+
program_name, timeout = program_name
124+
with open(os.dup(input.input_file.fileno()), 'r', newline='\n') as input_file:
125+
if timeout is None:
126+
content = make_unicode(subprocess.check_output(program_name, shell=(not list_like(program_name)), stdin=input_file, universal_newlines=True))
127+
else:
128+
content = make_unicode(subprocess.check_output(program_name, shell=(not list_like(program_name)), stdin=input_file, universal_newlines=True, timeout=timeout))
129+
cls.__compare_two(program_name, content, std, grader)
130+
131+
if job_pool is not None:
132+
job_pool.map(do, programs)
133+
else:
134+
[x for x in map(do, programs)]

cyaron/graders/fulltext.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import hashlib
22
from .graderregistry import CYaRonGraders
3+
from .mismatch import HashMismatch
34

45
@CYaRonGraders.grader("FullText")
56
def fulltext(content, std):
67
content_hash = hashlib.sha256(content.encode('utf-8')).hexdigest()
78
std_hash = hashlib.sha256(std.encode('utf-8')).hexdigest()
8-
return (True, None) if content_hash == std_hash else (False, "Hash mismatch: read %s, expected %s" % (content_hash, std_hash))
9+
return (True, None) if content_hash == std_hash else (False, HashMismatch(content, std, content_hash, std_hash))
910

cyaron/graders/mismatch.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
class Mismatch(ValueError):
2+
"""exception for content mismatch"""
3+
def __init__(self, content, std, *args):
4+
"""
5+
content -> content got
6+
std -> content expected
7+
"""
8+
super(Mismatch, self).__init__(content, std, *args)
9+
self.content = content
10+
self.std = std
11+
12+
class HashMismatch(Mismatch):
13+
"""exception for hash mismatch"""
14+
def __init__(self, content, std, content_hash, std_hash):
15+
"""
16+
content -> content got
17+
std -> content expected
18+
content_hash -> hash of content
19+
std_hash -> hash of std
20+
"""
21+
super(HashMismatch, self).__init__(content, std, content_hash, std_hash)
22+
self.content_hash = content_hash
23+
self.std_hash = std_hash
24+
25+
def __str__(self):
26+
return "Hash mismatch: read %s, expected %s" % (self.content_hash, self.std_hash)
27+
28+
class TextMismatch(Mismatch):
29+
"""exception for text mismatch"""
30+
def __init__(self, content, std, err_msg, lineno=None, colno=None, content_token=None, std_token=None):
31+
"""
32+
content -> content got
33+
std -> content expected
34+
err_msg -> error message template like "wrong on line {} col {} read {} expected {}"
35+
lineno -> line number
36+
colno -> column number
37+
content_token -> the token of content mismatch
38+
std_token -> the token of std
39+
"""
40+
super(TextMismatch, self).__init__(content, std, err_msg, lineno, colno, content_token, std_token)
41+
self.err_msg = err_msg.format(lineno, colno, content_token, std_token)
42+
self.lineno = lineno
43+
self.colno = colno
44+
self.content_token = content_token
45+
self.std_token = std_token
46+
47+
def __str__(self):
48+
return self.err_msg

cyaron/graders/noipstyle.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
from ..utils import *
22
from .graderregistry import CYaRonGraders
3+
from .mismatch import TextMismatch
34

45

56
@CYaRonGraders.grader("NOIPStyle")
67
def noipstyle(content, std):
78
content_lines = strtolines(content.replace('\r\n', '\n'))
89
std_lines = strtolines(std.replace('\r\n', '\n'))
910
if len(content_lines) != len(std_lines):
10-
return False, 'Too many or too few lines.'
11+
return False, TextMismatch(content, std, 'Too many or too few lines.')
1112

1213
for i in range(len(content_lines)):
1314
if std_lines[i] != content_lines[i]:
1415
for j in range(min(len(std_lines[i]), len(content_lines[i]))):
1516
if std_lines[i][j] != content_lines[i][j]:
16-
return (False, 'On line %d column %d, read %s, expected %s.'
17-
% (i + 1, j + 1, content_lines[i][j:j + 5], std_lines[i][j:j + 5]))
17+
return (False, TextMismatch(content, std, 'On line {} column {}, read {}, expected {}.',
18+
i + 1, j + 1, content_lines[i][j:j + 5], std_lines[i][j:j + 5]))
1819
if len(std_lines[i]) > len(content_lines[i]):
19-
return False, 'Too short on line %d.' % i
20+
return False, TextMismatch(content, std, 'Too short on line {}.', i)
2021
if len(std_lines[i]) < len(content_lines[i]):
21-
return False, 'Too long on line %d.' % i
22+
return False, TextMismatch(content, std, 'Too long on line {}.', i)
2223

23-
return True, None
24+
return True, None

0 commit comments

Comments
 (0)