As part of dissolving our sub-projects in edx-platform, we are moving this package under the xmodule directory. We have fixed all the occurences of import of this package and also fixed all documents related references. This might break your platform if you have any reference of `import capa` or `from capa import` in your codebase or in any Xblock. Ref: https://openedx.atlassian.net/browse/BOM-2582
161 lines
6.2 KiB
Python
Executable File
161 lines
6.2 KiB
Python
Executable File
#!/usr/bin/env python
|
|
"""
|
|
Commandline tool for doing operations on Problems
|
|
"""
|
|
|
|
|
|
import argparse
|
|
import logging
|
|
import sys
|
|
from io import BytesIO
|
|
|
|
from calc import UndefinedVariable
|
|
from mako.lookup import TemplateLookup
|
|
from path import Path as path
|
|
|
|
from xmodule.capa.capa_problem import LoncapaProblem
|
|
|
|
logging.basicConfig(format="%(levelname)s %(message)s")
|
|
log = logging.getLogger('capa.checker')
|
|
|
|
|
|
class DemoSystem(object): # lint-amnesty, pylint: disable=missing-class-docstring
|
|
def __init__(self):
|
|
self.lookup = TemplateLookup(directories=[path(__file__).dirname() / 'templates'])
|
|
self.DEBUG = True
|
|
|
|
def render_template(self, template_filename, dictionary):
|
|
"""
|
|
Render the specified template with the given dictionary of context data.
|
|
"""
|
|
return self.lookup.get_template(template_filename).render(**dictionary)
|
|
|
|
|
|
def main(): # lint-amnesty, pylint: disable=missing-function-docstring
|
|
parser = argparse.ArgumentParser(description='Check Problem Files')
|
|
parser.add_argument("command", choices=['test', 'show']) # Watch? Render? Open?
|
|
parser.add_argument("files", nargs="+", type=argparse.FileType('r'))
|
|
parser.add_argument("--seed", required=False, type=int)
|
|
parser.add_argument("--log-level", required=False, default="INFO",
|
|
choices=['info', 'debug', 'warn', 'error',
|
|
'INFO', 'DEBUG', 'WARN', 'ERROR'])
|
|
|
|
args = parser.parse_args()
|
|
log.setLevel(args.log_level.upper())
|
|
|
|
system = DemoSystem()
|
|
|
|
for problem_file in args.files:
|
|
log.info("Opening {0}".format(problem_file.name))
|
|
|
|
try:
|
|
problem = LoncapaProblem(problem_file, "fakeid", seed=args.seed, system=system) # lint-amnesty, pylint: disable=no-value-for-parameter, unexpected-keyword-arg
|
|
except Exception as ex: # lint-amnesty, pylint: disable=broad-except
|
|
log.error("Could not parse file {0}".format(problem_file.name))
|
|
log.exception(ex)
|
|
continue
|
|
|
|
if args.command == 'test':
|
|
command_test(problem)
|
|
elif args.command == 'show':
|
|
command_show(problem)
|
|
|
|
problem_file.close()
|
|
|
|
# In case we want to do anything else here.
|
|
|
|
|
|
def command_show(problem):
|
|
"""Display the text for this problem"""
|
|
print(problem.get_html())
|
|
|
|
|
|
def command_test(problem): # lint-amnesty, pylint: disable=missing-function-docstring
|
|
# We're going to trap stdout/stderr from the problems (yes, some print)
|
|
old_stdout, old_stderr = sys.stdout, sys.stderr
|
|
try:
|
|
sys.stdout = BytesIO()
|
|
sys.stderr = BytesIO()
|
|
|
|
check_that_suggested_answers_work(problem)
|
|
check_that_blanks_fail(problem)
|
|
|
|
log_captured_output(sys.stdout,
|
|
"captured stdout from {0}".format(problem))
|
|
log_captured_output(sys.stderr,
|
|
"captured stderr from {0}".format(problem))
|
|
except Exception as e: # lint-amnesty, pylint: disable=broad-except
|
|
log.exception(e)
|
|
finally:
|
|
sys.stdout, sys.stderr = old_stdout, old_stderr
|
|
|
|
|
|
def check_that_blanks_fail(problem):
|
|
"""Leaving it blank should never work. Neither should a space."""
|
|
blank_answers = dict((answer_id, "")
|
|
for answer_id in problem.get_question_answers())
|
|
grading_results = problem.grade_answers(blank_answers)
|
|
try:
|
|
assert all(result == 'incorrect' for result in grading_results.values())
|
|
except AssertionError:
|
|
log.error("Blank accepted as correct answer in {0} for {1}"
|
|
.format(problem,
|
|
[answer_id for answer_id, result
|
|
in sorted(grading_results.items())
|
|
if result != 'incorrect']))
|
|
|
|
|
|
def check_that_suggested_answers_work(problem):
|
|
"""Split this up so that we're only used for formula/numeric answers.
|
|
|
|
Examples of where this fails:
|
|
* Displayed answers use units but acceptable ones do not.
|
|
- L1e0.xml
|
|
- Presents itself as UndefinedVariable (when it tries to pass to calc)
|
|
* "a or d" is what's displayed, but only "a" or "d" is accepted, not the
|
|
string "a or d".
|
|
- L1-e00.xml
|
|
"""
|
|
# These are actual answers we get from the responsetypes
|
|
real_answers = problem.get_question_answers()
|
|
|
|
# all_answers is real_answers + blanks for other answer_ids for which the
|
|
# responsetypes can't provide us pre-canned answers (customresponse)
|
|
all_answer_ids = problem.get_answer_ids()
|
|
all_answers = dict((answer_id, real_answers.get(answer_id, ""))
|
|
for answer_id in all_answer_ids)
|
|
|
|
log.debug("Real answers: {0}".format(real_answers))
|
|
if real_answers:
|
|
try:
|
|
real_results = dict((answer_id, result) for answer_id, result
|
|
in problem.grade_answers(all_answers).items()
|
|
if answer_id in real_answers)
|
|
log.debug(real_results)
|
|
assert(all(result == 'correct'
|
|
for answer_id, result in real_results.items()))
|
|
except UndefinedVariable as uv_exc:
|
|
log.error("The variable \"{0}\" specified in the ".format(uv_exc) + # lint-amnesty, pylint: disable=logging-not-lazy
|
|
"solution isn't recognized (is it a units measure?).")
|
|
except AssertionError:
|
|
log.error("The following generated answers were not accepted for {0}:"
|
|
.format(problem))
|
|
for question_id, result in sorted(real_results.items()):
|
|
if result != 'correct':
|
|
log.error(" {0} = {1}".format(question_id, real_answers[question_id]))
|
|
except Exception as ex: # lint-amnesty, pylint: disable=broad-except
|
|
log.error("Uncaught error in {0}".format(problem))
|
|
log.exception(ex)
|
|
|
|
|
|
def log_captured_output(output_stream, stream_name): # lint-amnesty, pylint: disable=missing-function-docstring
|
|
output_stream.seek(0)
|
|
output_text = output_stream.read()
|
|
if output_text:
|
|
log.info("##### Begin {0} #####\n".format(stream_name) + output_text) # lint-amnesty, pylint: disable=logging-not-lazy
|
|
log.info("##### End {0} #####".format(stream_name))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|