1、直接在anaconda prompt运行pytest
(前提是已经安装pytest,也可以用colab
2、若要使用–json-report等argument,需要安装pytest插件
pip install pytest-json-report --upgrade
3、若要使用–suppress-tests-failed-exit-code
!pip install pytest-custom-exit-code
4、若要使用jq,需要安装
以colab为例,colab用的是Ubuntu 18.04.6 LTS (Bionic Beaver)容器
!cat /etc/os-release
在Ubuntu下可以使用如下命令安装
!apt-get install jq
如果要安装的是pyjq,则需要先安装autoconf libtool
!apt-get install autoconf libtool
然后安装pyjq
!pip install pyjq
将测试写入.sh文件,可以看到里面用了各种arguments以及jq工具
#!/bin/bash
OUTPUT=/tmp/hw1p1.log
REPORT=report.json
PYTEST_OPTS=""
PYTEST_OPTS="$PYTEST_OPTS --cache-clear -rA --capture=no --show-capture=no --tb=short"
PYTEST_OPTS="$PYTEST_OPTS --json-report --json-report-file=$REPORT --json-report-omit collectors keywords"
PYTEST_OPTS="$PYTEST_OPTS --suppress-tests-failed-exit-code"
start=`date +%s`
pytest $PYTEST_OPTS . > "$OUTPUT" 2>&1
#python3 -u hw1p1-soln.py
status=$?
end=`date +%s`
echo "Runtime: $((end-start))"
cat $OUTPUT
# checkout output status
if [ ${status} -ne 0 ]; then
echo "Failure: fails or returns nonzero exit status of ${status}"
echo '{"scores": {"Success": 0}}'
exit
fi
# include any post-parsing here
NFAIL=$(jq -r '.summary.failed | select(.!=null)' < report.json)
NPASS=$(jq -r '.summary.passed | select(.!=null)' < report.json)
NTOTAL=$(jq -r '.summary.total | select(.!=null)' < report.json)
echo '{"scores": {"Success":1, "Pass":'${NPASS:=0}', "Fail":'${NFAIL:=0}', "Score":'$((100 * $NPASS/${NTOTAL:=1}))'}}'
exit
辅助文件 test_hw1p1.py
import subprocess, string
import pytest
EXEC='python3'
SCRIPT_TO_TEST='./hw1p1.py'
# PROBLEM_ID: STRING
STRINGS_TO_TEST = [
'string',
'space string',
'stringstring',
'stringstringstring',
'aaaaaaaaaaaaaaaaaa',
'foo1bar',
'',
'Loremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliqua',
'.',
' ',
'invalid^'
]
EXPECT_RETURNCODE = 0
# only supports text stdout/stderr
def run_test_script(args):
# check args list
if not isinstance(args, list):
args = [args]
run = [EXEC, SCRIPT_TO_TEST] + args
# start script
ps = subprocess.Popen(run, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
ps.wait()
[stdout, stderr] = [x.decode('utf-8').strip() for x in ps.communicate()]
returncode = int(ps.returncode)
return (returncode, stdout, stderr)
# r = k!
def factorial(k):
r = 1
for i in range(1, k+1):
r *= i
return r
def count_anagrams(s):
# normalize
s = s.lower()
# "histogram"
hist = [0] * 26
for k, letter in enumerate(string.ascii_lowercase):
hist[k] = s.count(letter)
# multinomial log-implementation
perm1 = factorial(sum(hist))
# note, long strings need arbitrary precision
# published first without, so return both and count either correct
perm2 = perm1
for count in hist:
perm1 /= factorial(count)
perm2 //= factorial(count)
return [int(perm1), int(perm2)]
def is_valid(s):
return not s or s.isalpha()
# does not check valid, only anagram count => number / empty
def assert_stdout(s, stdout):
if not is_valid(s):
assert stdout == '', f'string "{s}" STDOUT: expected=, actual={stdout}'
elif len(s) == 0:
assert stdout == 'empty', f'string "{s}" STDOUT: expected=empty, actual={stdout}'
else:
# note, see comment above
count1, count2 = count_anagrams(s)
assert stdout == f'{count1}' or stdout == f'{count2}'
def assert_stderr(s, stderr):
if not is_valid(s):
assert stderr == 'invalid', f'string "{s}" STDERR: expected=invalid, actual={stderr}'
else:
assert stderr == '', f'string "{s}" STDERR: expected=, actual={stderr}'
@pytest.mark.parametrize('s', [s for s in STRINGS_TO_TEST])
def test_string(s):
returncode, stdout, stderr = run_test_script(s)
# if return code does not match AND == 1, assume program crashed, return error
if returncode != EXPECT_RETURNCODE and returncode == 1:
raise Exception(stderr)
assert returncode == EXPECT_RETURNCODE, f'string "{s}" returncode:expected={EXPECT_RETURNCODE}, actual={returncode}'
assert_stderr(s, stderr)
assert_stdout(s, stdout)
report.json
{"created": 1662063651.473876, "duration": 0.5840983390808105, "exitcode": 0, "root": "/content", "environment": {"Python": "3.7.13", "Platform": "Linux-5.4.188+-x86_64-with-Ubuntu-18.04-bionic", "Packages": {"pytest": "7.1.2", "py": "1.11.0", "pluggy": "1.0.0"}, "Plugins": {"json-report": "1.5.0", "custom-exit-code": "0.3.0", "metadata": "2.0.2", "typeguard": "2.7.1"}}, "summary": {"passed": 11, "total": 11, "collected": 11}, "tests": [{"nodeid": "test_hw1p1.py::test_string[string]", "lineno": 92, "outcome": "passed", "setup": {"duration": 0.0003476919999911843, "outcome": "passed"}, "call": {"duration": 0.04421803299999283, "outcome": "passed"}, "teardown": {"duration": 0.0002795460000015737, "outcome": "passed"}}, {"nodeid": "test_hw1p1.py::test_string[space string]", "lineno": 92, "outcome": "passed", "setup": {"duration": 0.00032319900003585644, "outcome": "passed"}, "call": {"duration": 0.041243673000053604, "outcome": "passed"}, "teardown": {"duration": 0.00024495399998158973, "outcome": "passed"}}, {"nodeid": "test_hw1p1.py::test_string[stringstring]", "lineno": 92, "outcome": "passed", "setup": {"duration": 0.00033566100000825827, "outcome": "passed"}, "call": {"duration": 0.04028028899995206, "outcome": "passed"}, "teardown": {"duration": 0.00024724699994749244, "outcome": "passed"}}, {"nodeid": "test_hw1p1.py::test_string[stringstringstring]", "lineno": 92, "outcome": "passed", "setup": {"duration": 0.000330002000055174, "outcome": "passed"}, "call": {"duration": 0.0426553290000129, "outcome": "passed"}, "teardown": {"duration": 0.00026272699994933646, "outcome": "passed"}}, {"nodeid": "test_hw1p1.py::test_string[aaaaaaaaaaaaaaaaaa]", "lineno": 92, "outcome": "passed", "setup": {"duration": 0.0003462529999751496, "outcome": "passed"}, "call": {"duration": 0.04282293200003551, "outcome": "passed"}, "teardown": {"duration": 0.0002480279999872437, "outcome": "passed"}}, {"nodeid": "test_hw1p1.py::test_string[foo1bar]", "lineno": 92, "outcome": "passed", "setup": {"duration": 0.00032949400008419616, "outcome": "passed"}, "call": {"duration": 0.04206459699992138, "outcome": "passed"}, "teardown": {"duration": 0.00026792599999225786, "outcome": "passed"}}, {"nodeid": "test_hw1p1.py::test_string[]", "lineno": 92, "outcome": "passed", "setup": {"duration": 0.0003373019999344251, "outcome": "passed"}, "call": {"duration": 0.0413927589999048, "outcome": "passed"}, "teardown": {"duration": 0.00025381800003287935, "outcome": "passed"}}, {"nodeid": "test_hw1p1.py::test_string[Loremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliqua]", "lineno": 92, "outcome": "passed", "setup": {"duration": 0.000335223999968548, "outcome": "passed"}, "call": {"duration": 0.04252076099999158, "outcome": "passed"}, "teardown": {"duration": 0.00024685599998974794, "outcome": "passed"}}, {"nodeid": "test_hw1p1.py::test_string[.]", "lineno": 92, "outcome": "passed", "setup": {"duration": 0.0003379639999820938, "outcome": "passed"}, "call": {"duration": 0.04881728899999871, "outcome": "passed"}, "teardown": {"duration": 0.00025473300001976895, "outcome": "passed"}}, {"nodeid": "test_hw1p1.py::test_string[ ]", "lineno": 92, "outcome": "passed", "setup": {"duration": 0.00035387100001571525, "outcome": "passed"}, "call": {"duration": 0.043154117000085535, "outcome": "passed"}, "teardown": {"duration": 0.0002428340000051321, "outcome": "passed"}}, {"nodeid": "test_hw1p1.py::test_string[invalid^]", "lineno": 92, "outcome": "passed", "setup": {"duration": 0.0003739240000868449, "outcome": "passed"}, "call": {"duration": 0.04142785100009405, "outcome": "passed"}, "teardown": {"duration": 0.00030338299995946727, "outcome": "passed"}}]}
被测试代码 hw1p1.py
import sys
def isalpha(s):
len_ = len(s)
isletter = lambda ch: True if 97<=ord(ch)<=122 else False
if sum(list(map(isletter, s))) == len_:
return True
return False
def validate():
# check if empty
try:
# check input string is there
assert len(sys.argv) > 1
s = sys.argv[1].lower()
assert len(s)
except:
sys.stdout.write("empty")
sys.exit()
# check if valid format
try:
s = sys.argv[1].lower()
assert isalpha(s)
except:
sys.stderr.write("invalid")
sys.exit()
def factorial(n):
return 1 if n == 0 or n == 1 else n*factorial(n-1)
def combination(n, r):
return factorial(n)//(factorial(r)*factorial(n-r))
def permutation(n, r):
return factorial(n)//factorial(n-r)
def count_unique_anagram(s: str) -> int:
# start from the smallest anagram
s = sorted(s)
# accumulate count, and initialize room for rearrangement
count, room = 1, len(s)
# count letters
ch2count = [0]*26 # 0: a, 1: b, ..., 25: z
for ch in s:
ch2count[ord(ch)-97] += 1
# sort for combination first
ch2count.sort(reverse=True)
num_of_1 = 0
for i, c in enumerate(ch2count):
if c > 1: # combination first for letters that appears multiple
count *= combination(room, c)
room -= c
elif c > 0: # count number of letter that only appears once
num_of_1 += 1
# do permuatation
if num_of_1:
count *= permutation(room, num_of_1)
return count
if __name__ == "__main__":
validate()
# case-insensitive
unique_num = count_unique_anagram(sys.argv[1].lower())
sys.stdout.write(f"{unique_num}")