Beispiel #1
0
def replymail_error( msg, text, CC_to_admin=False, maxsend=None ):
    """Takes message 'msg' and composes a reply to the sender with body 'text'.

    If CC_to_admin is true, the message will be sent to the sysadmin as well.

    The idea of maxsend is that if maxsend is given, we will only attempt maxsend time
    to deliver email to the same email address. This could be useful if we engage in
    infinite loops with external spam. The code isn't written for this yet.
    """

    real_name, email_addr = email.Utils.parseaddr( msg["From"] )

    log_global.debug("in replymail_error (to %s, subj: %s)" % (real_name,msg["Subject"]))

    text = "Dear "+str(real_name)+" ("+str(email_addr)+"),\n\n" + \
           "An error occured while parsing your email with subject\n" + \
           repr(msg["Subject"])+" received at "+time.asctime()+":\n\n"+ \
           text

    if CC_to_admin:
        subject = "["+conf.ModulecodeSubjectLine+"-admin] submission error, "+time.ctime(time.time())+", "+str(msg["Subject"])
        enqueue_outgoing_mails.send_text_message( conf.SysadminEmail, conf.ModuleEmailAddress, text, subject)

    subject = "["+conf.ModulecodeSubjectLine+"] submission error, "+time.ctime(time.time())+", "+str(msg["Subject"])

    return enqueue_outgoing_mails.send_text_message( email_addr, conf.ModuleEmailAddress, text, subject)
Beispiel #2
0
def process_one_subtest(job):
    student_lab_dir = job['student_lab_dir']
    testcodefile = conf.subtest_tests[job['assignment']]
    testcodepath = os.path.join(conf.subtest_testcodedir, job['assignment'],
                                testcodefile)

    all_submitted_files = conf.assignments[job['assignment']].keys()
    all_submitted_filepaths=[ os.path.join(student_lab_dir, fn) \
        for fn in all_submitted_files]

    submitted_filepath = None  # stop copying files to s.py

    log_global.info("Run py.test in %s " % student_lab_dir)

    try:
        if False:
            log_global.debug("Will run run_pytest (not constrained)")
            log_global.warn(
                "Will run run_pytest (not constrained) -- not secure"
            )  # student code could
            # damage files etc
            test_run_dir = subtest.run_pytest(
                submitted_filepath,
                testcodepath,
                rundirectorypath=student_lab_dir,
                jobfilepath=job['qfilepath'],
                maxseconds=conf.subtest_maxseconds,
                log_global=log_global)
        else:
            log_global.debug("Will run run_pytest_constrained")
            test_run_dir = subtest.run_pytest_constrained(
                submitted_filepath,
                testcodepath,
                rundirectorypath=student_lab_dir,
                other_submitted_filepaths=all_submitted_filepaths,
                jobfilepath=job['qfilepath'],
                maxseconds=conf.subtest_maxseconds,
                log_global=log_global,
                pytest_args=conf.pytest_additional_arguments)

    except PyTestException, msg:
        log_global.exception("pytest failed: %s" % msg)
        log_global.error("possible cause: error in test_*.py code?")
        ins, outs = os.popen4('tail -n 100 ' + conf.subtest_logfile)
        text = outs.read() + '\n' + 'Error message is:\n' + str(msg)
        subject = "Urgent: Malfunction in %s at %s (pytest reports problem)" % (
            conf.ModulecodeSubjectLine, time.asctime())
        enqueue_outgoing_mails.send_text_message(conf.SysadminEmail,
                                                 conf.ModuleEmailAddress, text,
                                                 subject)
        log_global.info("Emailed sysadmin (%s)" % conf.SysadminEmail)
        raise PyTestException, msg
Beispiel #3
0
def replymail_confirm_submission(real_name, email_addr, text, subject, assignment, valid_attachments, q_id=None):
    """ Sends an email to the student named real_name at address
    email_addr, which consists of a confirmation of receipt of their
    email whose subject should be in subject, with a q_id (if
    assigned) followed by the contents of the traing text.

    Returns the sent message as string if there are no attachments, or
    None."""

    intro = "Dear "+real_name+" ("+email_addr+"),\n\n" + \
    textwrap.fill("this email confirms the receipt of your email\n" + \
    "with subject %s at %s." %( repr(subject), time.asctime()))+"\n\n"

    if q_id:
        if valid_attachments:
            intro += textwrap.fill("Your submitted files have been added to the "+\
                "testing queue (id=%s).\n" % (q_id) + \
                "You will receive a separate email with the "+\
                "testing results.")+"\n\n"

        newsubject = "["+conf.ModulecodeSubjectLine+"] Submission Confirmation "\
            +str(assignment)+" ("+time.asctime()+")"

    else:
        intro += textwrap.fill("Your files will be archived.")+"\n\n"
        newsubject = "["+conf.ModulecodeSubjectLine+"] Archive confirmation "+str(assignment)+" ("+time.asctime()+")"

    return enqueue_outgoing_mails.send_text_message( email_addr, conf.ModuleEmailAddress, intro+text, newsubject)
def send_email_from_file(filename):
    """Reads filename, which is expected to be in the format returned
    by the as_string() method of an email.Message.Message obejct,
    parses it into an email.Message.Message object as an approximation
    of validation, and tries to send it via SMTP.  On success it
    returns os.EX_OK, otherwise it returns None or a failure code from
    the os module."""

    # Read file as string
    with open(filename,'r') as f:
        msg_string = f.read()

    # Parse string as email message, ensure that the message has at
    # least certain fields filled in, move to the rejected directory
    # and warn administrator if we fail.
    try:
        msg = email.message_from_string(msg_string)
        assert msg["To"].count("@") > 0
        assert msg["From"].count("@") > 0
        assert len(msg["Subject"]) > 0
        assert len(msg.get_payload()) > 0

    except Exception, errmesg:
        log_global.warn("Parsing email from queued file {0:s} failed, "
                        "rejecting. ({1:s})".format(filename, errmesg))

        text = ("WARNING: invalid entry in outgoing mail queue.\n"
                "Exception message was: {0:s}\n"
                "The rest of this message contains the entry.\n".format(errmesg))
        text += "\n" + 40*"=" + "\n"
        text += msg_string + "\n" + 40*"=" + "\n"
        subject = ("WARNING: Invalid outgoing mail found in queue in "
                   "{0:s} at {1:s}".format(conf.ModulecodeSubjectLine,
                                           time.asctime()))

        enqueue_outgoing_mails.send_text_message(conf.SysadminEmail, conf.ModuleEmailAddress,text, subject)

        name_only = os.path.split(filename)[-1]

        if not os.path.exists(conf.outgoingmail_rejected):
            log_global.info("Directory for rejected outgoing mail {0:s} "
                            "does not exist, will create it now.".format(conf.outgoingmail_rejected))
            os.makedirs(conf.outgoingmail_rejected)

        shutil.move(filename, os.path.join(conf.outgoingmail_rejected, name_only))
        return os.EX_DATAERR
    def email_admin_smtp_socket_err(continuing=0):
        ins,outs = os.popen4('tail -n 100 '+conf.outgoingmail_logfile)

        text = ("There have been {0:d} consecutive SMTP socket errors "
                "since {1:s}\n\n".format(smtp_socket_errors['smtp_failed_run_count'],
                                       smtp_socket_errors['smtp_first_fail_time']))
        text += "SMTP errors with their counts were:\n"
        for this_err in set(smtp_socket_errors['smtp_error_messages']):
            text += repr ((this_err, smtp_socket_errors['smtp_error_messages'].count(this_err))) + "\n"
        text += "\nRecent outgoing mail processor log file entries follow:\n\n" + outs.read()

        if(continuing==0):
            subject = ("WARNING: SMTP socket error - malfunction in {0:s} at {1:s}"
                       "!!!".format(conf.ModulecodeSubjectLine,time.asctime()))
        else:
            subject = ("WARNING: Continuing SMTP socket error - malfunction in {0:s} at {1:s}"
                       "!!!".format(conf.ModulecodeSubjectLine,time.asctime()))

        enqueue_outgoing_mails.send_text_message(conf.SysadminEmail, conf.ModuleEmailAddress, text, subject)
def record_smtp_socket_success():
    """Resets the SMTP socket error statistics file - called when
    there are no SMTP errors in a session."""

    smtp_socket_errors = read_smtp_socket_errfile()

    # If there were errors before this success, notify admin we
    # succeeded and are about to reset error counters.  Also log the
    # fact.
    if (smtp_socket_errors.has_key('smtp_failed_run_count')):
        err_count = smtp_socket_errors['smtp_failed_run_count']
    else:
        err_count = 0

    if err_count > 0:
        subject = ("INFORMATION: SMTP socket error cleared in {0:s} "
                   "at {1:s}".format(conf.ModulecodeSubjectLine,time.asctime()))
        text = ("After {0:d} SMTP socket errors, delivery of emails has succeeded.\n"
                "Will reset error counters.".format(err_count))

        enqueue_outgoing_mails.send_text_message(conf.SysadminEmail, conf.ModuleEmailAddress, text, subject, conf )

        log_global.info("After {0:d} failures, SMTP socket error cleared".format(err_count))
        log_global.info("Emailed sysadmin")

    # Clear error counter and timestamps, set success time to now.
    smtp_socket_errors = {
        'smtp_failed_run_count':0,
        'smtp_first_fail_time':'',
        'smtp_last_fail_time':'',
        'smtp_last_success_time':time.asctime(),
        'smtp_last_admin_mail_time':time.asctime(),
        'smtp_error_messages':[]
        }

    # Save to disk.
    with open(conf.smtp_error_file,'w') as f:
        fcntl.flock(f.fileno(), fcntl.LOCK_EX)
        f.seek(0)
        f.truncate(0)
        cPickle.dump(smtp_socket_errors, f)
        fcntl.flock(f.fileno(), fcntl.LOCK_UN)
Beispiel #7
0
def check_maildaemon( msg, from_addr, domain ):

    if re.search(_rx_email_daemon,from_addr,re.IGNORECASE):
        log_global.critical("Received a msg from a mailing daemon (X)! ("+msg["From"]+")")
        log_global.critical("Forwarding it to administrator (%s)." % (conf.SysadminEmail) )

        # We need to delete the "To" key entry first:
        del msg["To"]
        # Then write new value
        msg["To"] = conf.SysadminEmail

        original_subject = msg["Subject"] 
        del msg["Subject"]
        msg["Subject"] = "Urgent: [%s] email from daemon: %s" % (conf.ModulecodeSubjectLine,repr(original_subject))

        del msg["From"]
        msg["From"] = conf.ModuleEmailAddress

        enqueue_outgoing_mails.mailqueue_push(msg)
        return 1

    #check that the email is not from ourselves
    if string.count( from_addr.lower(), conf.Modulecode.lower()):
        log_global.critical("Received a msg from myself! ("+msg["From"]+")")
        log_global.critical("Forwarding it to administrator (%s)." % repr(conf.SysadminEmail) )

        subject = "Urgent: [%s] email from system (danger of loop): %s" % (conf.ModulecodeSubjectLine,repr(msg["Subject"]))
        sendmail = enqueue_outgoing_mails.send_text_message( conf.SysadminEmail, conf.ModuleEmailAddress, msg.as_string(), subject)
        append_mail_to_mailbox( sendmail, conf.sysadminmailfolder, log_global, "(outgoing mail to SysadminEmail (myself-loop))" )
        return 1

    if string.count(domain.lower(), "twitter"):
        log_global.info("Received a msg from twitter: ("+msg["From"]+")")
        log_global.info("Forwarding it to administrator (%s)." % (conf.SysadminEmail) )

        # We need to delete the "To" key entry first:
        del msg["To"]
        #Then write new value
        msg["To"] = conf.SysadminEmail

        original_subject = msg["Subject"] 
        del msg["Subject"]
        msg["Subject"] = "[%s] Twitter writes: %s" % (\
            conf.ModulecodeSubjectLine,repr(original_subject))

        del msg["From"]
        msg["From"] = conf.ModuleEmailAddress

        enqueue_outgoing_mails.mailqueue_push(msg)
        return 1

    return 0
Beispiel #8
0
def check_student_groups(students, groups, email_admin=True):
    """Given the dictionary 'students', {'stud1@domain1':('stud1
    name','stud1_group'), ...}, and the list 'groups', checks that the
    group assigned to each student can be found in 'groups'.  If this
    is not the case the error is logged and, by default, the
    administrator is emailed a warning.  Returns a new students
    dictionary which is the input students dictionary, less any
    assigned to invalid groups, and a list of those students that were
    removed."""

    output_students = {}
    ignored_students = []

    for stud_email in students:
        (stud_name, stud_group) = students[stud_email]
        if stud_group not in groups:
            ignored_students.append("{0} ({1}) (group: {2})".format(
                stud_name, stud_email, stud_group))
            log_global.error("Student '{0}' ({1}) assigned group '{2}',"
                             " which is not in config file '{3}'.".format(
                                 stud_name, stud_email, stud_group,
                                 'config_' + Modulecode.lower()))
            if email_admin:
                subject = (
                    "WARNING: {0}: student assigned to invalid group {1}".
                    format(conf.ModulecodeSubjectLine, stud_group))
                text = (
                    "The student {0} ({1}) is assigned to deadline group {2},"
                    " which was not found in the configuration file {3}.  Marks for"
                    " this student will not be recorded.".format(
                        stud_name, stud_email, stud_group,
                        'config_' + Modulecode.lower()))
                text = textwrap.fill(text)
                enqueue_outgoing_mails.send_text_message(
                    conf.SysadminEmail, conf.ModuleEmailAddress, text, subject)
        else:
            output_students[stud_email] = students[stud_email]

    return (output_students, ignored_students)
Beispiel #9
0
def email_address_username_and_domain(addr):
    """Maps an email address such as [email protected] to username
    and domain part. 

    Note: this routine splits at the last '@' sign, having checked
    only that the given address contains at least one '@' sign and at
    least one '.' character.

    It is possible to have a valid email address with multiple '@'
    signs, (see e.g. the informal RFC3696), and this routine should
    work in these cases.
    """

    if (addr.count('@') < 1 or addr.count('.') < 1):
        subject = "WARNING: Invalid address on incoming email from %s" % repr(addr)
        text = ("Email from %s, address either has fewer than one '.' character,\n"
                "or fewer than one '@' character." % repr(addr))
        enqueue_outgoing_mails.send_text_message(conf.SysadminEmail, conf.ModuleEmailAddress, text, subject)
        log_global.info("Emailed sysadmin about regex failure of splitting address %s. " % addr)
        raise StandardError,"Unusual email address : '%s" % repr(addr)

    try:
        parts = addr.split('@')
        domain = parts[-1]
        username = ''
        for p in parts[0:-2]:
            username = username + p + '@'
        username = username + parts[-2]
    except:
        # otherwise send message to admin
        subject = "WARNING: Address split failed on incoming email from %s" % repr(addr)
        text = "Email from %s. We split to find name='%s' and domain='%s'" % (addr,username,domain)
        enqueue_outgoing_mails.send_text_message(conf.SysadminEmail, conf.ModuleEmailAddress, text, subject)
        log_global.info("Emailed sysadmin about failure of splitting address %s. " % addr)
        raise StandardError,"Unusual email address : '%s" % repr(addr)

    return username, domain
Beispiel #10
0
def retrieve_assignment(assignment,message,student_dir,real_name,email_addr,logger):

    logger.info("Retrieving files for %s" % repr(assignment))

    (username,domain)=email_address_username_and_domain(email_addr)

    #get list of files in student_dir
    submission_dir = os.path.join(student_dir,assignment)

    if os.path.exists ( submission_dir ):
        submitted_files = os.listdir( submission_dir  )
    else:
        errormail = replymail_error( message, "It seems that you have not yet submitted any files." )
        append_mail_to_mailbox( errormail, username, logger, "(outgoing error mail: no files submitted->retrieval is impossible)" )
        return None

    files_by_type = analyze_filenames(assignment_file_map(assignment), submitted_files, logger)

    report = submitted_files_report(assignment, files_by_type)

    body  = ["Dear %s (%s),\n\n" % (real_name,email_addr)]

    body.append("Here is a list of your files found on the server for assignment '%s':\n\n" % assignment)
    body.append(report)

    body.append("\n\nPlease find attached to the _next_ email these files\n")
    body.append("that you submitted for '%s'.\n\n" %assignment)
    body.append("(In addition, there may be one (or more) files named\n")
    body.append("'part00?.bin' which contain the body of your email and can \n")
    body.append("be ignored.)\n\n")

    subject = "[%s] summary of submitted files for '%s' (%s)" % ( conf.ModulecodeSubjectLine, assignment, time.asctime()) 


    mail = enqueue_outgoing_mails.send_text_message( email_addr, conf.ModuleEmailAddress, string.join(body,""), subject)
    append_mail_to_mailbox( mail, username, logger, "(outgoing retrieval report mail)" )

    #now do retrieve the files and mail those

    subject = "[%s] retrieved files for '%s' (%s)" % ( conf.ModulecodeSubjectLine, assignment, time.asctime())
    From = conf.ModuleEmailAddress
    to = email_addr

    retrieval_return_mail = bundle_files_in_directory_in_email( submission_dir,to,From,subject)
    text =  enqueue_outgoing_mails.send_message(retrieval_return_mail)
    append_mail_to_mailbox( text, 'test', logger, "(outgoing retrieval mail)" )

    logger.info("Sent retrieval mail for %s" % repr(assignment))
Beispiel #11
0
def parse_pytest_report(dirpath, log):
    """Given the path to a pytest-directory, returns a tuple (A,B). The first entry A is the 
    report_data data structure:
      list L of triplets t with
        t[0] = True if test passed, False if failed
        t[1] = one line describing test (for example test/test_tenfunc.py:test_count)
        t[2] = None if test passed, otherwise
               String (including newlines) with more detailed failure description

    The second entry B is a list of bools. These are 'True' if py.test
    terminated and False if it didn't.

    If py.test did not terminate, the test data will be incomplete, but it is
    still useful to see which tests have been passed.
    """

    status, reason = conf.read_status(dirpath)

    log.debug("In parse_pytest_report, dirpath=%s" % dirpath)
    log.debug("DD status=%s" % status)
    log.debug("DD reason=%s" % reason)

    tests = []
    path = os.path.join(dirpath, conf.pytest_log)
    f = open(path, 'r')
    lines = f.readlines()
    f.close()
    n = len(lines)

    log.debug("Read {} lines from {}: \n'''{}'''".format(
        n, path, "\n".join(lines)))

    if reason == "unterminated":  #this happened when a job didn't terminate
        error_report = """When trying to test your submission, the
            testing code did not terminate within 60 seconds.

            Either your code runs a very lengthy calculation, or is stuck
            in some infinite loop.

            We cannot assess and test your code appropriately if it
            does not return.

            Please fix the problem (i.e. identify where it gets
            stuck), and resubmit, and inform the laboratory leader about
            this. If you don't understand any of this, please get also
            in touch with a demonstrator or the laboratory leader.

            Debug information:
              status = %s
              reason = %s
              Content of pytest_log:\n--\n%s
            --
            """ % (status, reason, "\n".join(lines))

        tests.append((False, "unterminated problem in s.py", error_report))
        log.warn("auto-testing-problem" + str(tests) + str(status) +
                 "Job has not terminated, need manual intervention??")

        return tests, status
    else:
        pass
        log.debug("Normal branch, have terminated.")

    # We may need this in two places further below
    error_report_import_failed = """
    When trying to import your submission, we have encountered an error,
    and as a result, this submission has failed. A likely problem is
    non-ASCII characters in your submission, or a syntax error, or 
    an operation that is carried out when the file is imported and which
    fails.
    
    All testing will fail if the file cannot be imported. It is thus
    important to remove all such errors before submission.
    
    We suggest that you either:
    
    - Execute the file on its own to check no errors are reported.
    
    - Ensure that you are not trying access particular data files that you have
      on your computer - these will generally not be available for the testing 
      system.
    
    - remove any characters that are not encoded as ASCII, and re-sumbit
      the file, or
    
    - specify the encoding of the file in accordance with the advice at
      http://www.python.org/dev/peps/pep-0263/
    
      For example, add the following comment on the first line of your
      file:
    
      # coding=utf-8
    
    If the above makes no sense (either because you don't understand it,
    or because you do understand but find this does not match reality),
    then please contact a demonstrator or the laboratory leader about
    this.
    """

    #special case:
    bug_1_description = """September 2011. It seems that syntax errors of this type:

    def my_foo(x):
         y = x**2
        return y

    which would result in an "IndentationError: unindent does not match any outer indentation level"
    can hang the testing with py.test (version 1.3).

    Upgrading py.test to 2.1.2 'solved' the hang issue. Py.test still fails with this behaviour (see tests/import-fails2.py):

      -  pytest_log is an empty file
      
      -  std error contains:


           Traceback (most recent call last):
           File "/usr/bin/py.test", line 9, in <module>
           load_entry_point('pytest==2.1.2', 'console_scripts', 'py.test')()
           File "/usr/local/lib/python2.6/dist-packages/pytest-2.1.2-py2.6.egg/_pytest/core.py", line 457, in main
           <snip>
           
           res = method(**kwargs)
           File "/usr/local/lib/python2.6/dist-packages/pytest-2.1.2-py2.6.egg/_pytest/resultlog.py", line 92, in pytest_internalerror
           path = excrepr.reprcrash.path
           AttributeError: 'str' object has no attribute 'reprcrash'.readlines()

      - stdout contains

        ============================= test session starts ==============================
	platform linux2 -- Python 2.6.6 -- pytest-2.1.2
	collecting ... INTERNALERROR> Traceback (most recent call last):
	INTERNALERROR>   File "/usr/local/lib/python2.6/dist-packages/pytest-2.1.2-py2.6.egg/_pytest/main.py", line 67, in wrap_session
	INTERNALERROR>     doit(config, session)
	INTERNALERROR>   File "/usr/local/lib/python2.6/dist-packages/pytest-2.1.2-py2.6.egg/_pytest/main.py", line 95, in _main
	INTERNALERROR>     config.hook.pytest_collection(session=session)
	INTERNALERROR>   File "/usr/local/lib/python2.6/dist-packages/pytest-2.1.2-py2.6.egg/_pytest/core.py", line 411, in __call__
	INTERNALERROR>     return self._docall(methods, kwargs)
        <snip>
	INTERNALERROR>   File "/usr/local/lib/python2.6/dist-packages/pytest-2.1.2-py2.6.egg/_pytest/resultlog.py", line 84, in pytest_collectreport
	INTERNALERROR>     longrepr = str(report.longrepr.reprcrash)
	INTERNALERROR> AttributeError: CollectErrorRepr instance has no attribute 'reprcrash'
	INTERNALERROR>

    The detailed files are available in code/python/subtest/bugs/bug-archive-test-import-fails2-py2.1.2

    So py.test seems to be buggy and ONLY in combination with the --reportlog switch. Decativing this --reportlog,
    gives the right error message.

    We will try to catch this here and ask the user(=student) to make sure they can 'run' their file through the
    interpreter without getting an error message.

    """

    if n == 0:  #the only case where we know this can happen is the bug 1 described above.
        log.info("n == 0: Reached branch for suspected BUG 1")

        #let's carry out more checks to see whether this is the case.
        pytest_stdout_string = open(os.path.join(dirpath,
                                                 conf.pytest_stdout)).read()
        if "INTERNALERROR" in pytest_stdout_string:
            log.info(
                "Found 'INTERNALERROR' in stdout --> supports suspected bug 1 with py.test 2.1 (try py.test --version)"
            )
            odd = 0
        else:
            log.warn(
                "Expected 'INTERNALERROR' in stdout but couldn't find. Don't understand."
            )
            odd = 1

        pytest_stderr_string = open(os.path.join(dirpath,
                                                 conf.pytest_stderr)).read()

        if """AttributeError""" in pytest_stderr_string and "reprcrash" in pytest_stderr_string:
            log.info(
                "Found 'AttributeError' in  stderr --> supports suspected bug 1 with py.test 2.1"
            )
            odd += 0
        else:
            odd += 1
            log.warn(
                "Expected 'AttributeError <...>` in stderr but couldn't find. Don't understand."
            )

        #Printinternalerror> AttributeError: CollectErrorRepr instance has no attribute 'reprcrash'

        assert status == True, "This must be true as the test has returned and was not killed."

        if odd == 0:  #clear case for Bug 1 -- no ODDities
            log.info("n == 0: We think import failed. ")

            error_report = """When trying to import your submission, we have
encountered an error, and as a result, this submission
has failed. A likely problem is indentation. 

You should find that Python reports some error 
(SyntaxError most likely) when you execute your file
MYFILE *before* submission (either by pressing F5 in IDLE,
or running "python MYFILE.py").

All testing will fail if the file cannot be imported. It
is thus important to remove all such errors before
submission.

If the above makes no sense (either because you don't understand
it, or because you do understand but find this does not match
reality), then please contact a demonstrator or the laboratory
leader about this."""

            tests.append((False, "import s.py", error_report))
            log.info("n == 0: Appending failed test for import")

            return tests, status
        else:
            raise RuntimeError,"A new bug has occured -- see logs for detail.\n"+\
            "In this case, the problem may originate from a py.test version newer than 2.1.2.\n"+\
            "We do a lot of tests to see whether a particular bug is a particular bug (see source for bug 1)\n"+\
            "and if any of the output changes significantly, this test will fail.\n\n"+\
            "The failed job should be the first one in the testing queue."

    #special case.
    bug_2_description = """March 2013. It seems that submissions containing non-ASCII characters
    which would usually result in an "SyntaxError: Non-ASCII character
    '\xff' in file..."  can cause problems in py.test 2.3.4, which
    fails (when called with the flags we use; running py.test without
    the "--resultlog=" flag set gives the correct error) with this
    behaviour:

      -  pytest_log contains the text in the pytest_log_bug2 variable (defined below).
      
      - pytest.stderr is an empty file          

      - pytest.stdout contains the text in the pytest_stdout_bug2 varaible (defined below).

    The detailed files are available in
    code/python/subtest/bugs/bug-archive-non-ascii-pytest2.3.4

    So py.test seems to be buggy and ONLY in combination with the
    --resultlog switch. Decativing this --resultlog, gives the right
    error message.

    We will try to catch this here and ask the user(=student) to make
    sure they can 'run' their file through the interpreter without
    getting an error message.
    
"""

    pytest_log_bug2 = """! /usr/local/lib/python2.7/dist-packages/_pytest/resultlog.py
 Traceback (most recent call last):
   File "/usr/local/lib/python2.7/dist-packages/_pytest/main.py", line 81, in wrap_session
     doit(config, session)
   File "/usr/local/lib/python2.7/dist-packages/_pytest/main.py", line 112, in _main
     config.hook.pytest_collection(session=session)
   File "/usr/local/lib/python2.7/dist-packages/_pytest/core.py", line 422, in __call__
     return self._docall(methods, kwargs)
   File "/usr/local/lib/python2.7/dist-packages/_pytest/core.py", line 433, in _docall
     res = mc.execute()
   File "/usr/local/lib/python2.7/dist-packages/_pytest/core.py", line 351, in execute
     res = method(**kwargs)
   File "/usr/local/lib/python2.7/dist-packages/_pytest/main.py", line 116, in pytest_collection
     return session.perform_collect()
   File "/usr/local/lib/python2.7/dist-packages/_pytest/main.py", line 465, in perform_collect
     items = self._perform_collect(args, genitems)
   File "/usr/local/lib/python2.7/dist-packages/_pytest/main.py", line 498, in _perform_collect
     self.items.extend(self.genitems(node))
   File "/usr/local/lib/python2.7/dist-packages/_pytest/main.py", line 643, in genitems
     node.ihook.pytest_collectreport(report=rep)
   File "/usr/local/lib/python2.7/dist-packages/_pytest/main.py", line 157, in call_matching_hooks
     return hookmethod.pcall(plugins, **kwargs)
   File "/usr/local/lib/python2.7/dist-packages/_pytest/core.py", line 426, in pcall
     return self._docall(methods, kwargs)
   File "/usr/local/lib/python2.7/dist-packages/_pytest/core.py", line 433, in _docall
     res = mc.execute()
   File "/usr/local/lib/python2.7/dist-packages/_pytest/core.py", line 351, in execute
     res = method(**kwargs)
   File "/usr/local/lib/python2.7/dist-packages/_pytest/resultlog.py", line 88, in pytest_collectreport
     longrepr = str(report.longrepr.reprcrash)
 AttributeError: CollectErrorRepr instance has no attribute 'reprcrash'"""

    pytest_stdout_bug2 = """============================= test session starts ==============================
platform linux2 -- Python 2.7.3 -- pytest-2.3.4
INTERNALERROR> Traceback (most recent call last):
INTERNALERROR>   File "/usr/local/lib/python2.7/dist-packages/_pytest/main.py", line 81, in wrap_session
INTERNALERROR>     doit(config, session)
INTERNALERROR>   File "/usr/local/lib/python2.7/dist-packages/_pytest/main.py", line 112, in _main
INTERNALERROR>     config.hook.pytest_collection(session=session)
INTERNALERROR>   File "/usr/local/lib/python2.7/dist-packages/_pytest/core.py", line 422, in __call__
INTERNALERROR>     return self._docall(methods, kwargs)
INTERNALERROR>   File "/usr/local/lib/python2.7/dist-packages/_pytest/core.py", line 433, in _docall
INTERNALERROR>     res = mc.execute()
INTERNALERROR>   File "/usr/local/lib/python2.7/dist-packages/_pytest/core.py", line 351, in execute
INTERNALERROR>     res = method(**kwargs)
INTERNALERROR>   File "/usr/local/lib/python2.7/dist-packages/_pytest/main.py", line 116, in pytest_collection
INTERNALERROR>     return session.perform_collect()
INTERNALERROR>   File "/usr/local/lib/python2.7/dist-packages/_pytest/main.py", line 465, in perform_collect
INTERNALERROR>     items = self._perform_collect(args, genitems)
INTERNALERROR>   File "/usr/local/lib/python2.7/dist-packages/_pytest/main.py", line 498, in _perform_collect
INTERNALERROR>     self.items.extend(self.genitems(node))
INTERNALERROR>   File "/usr/local/lib/python2.7/dist-packages/_pytest/main.py", line 643, in genitems
INTERNALERROR>     node.ihook.pytest_collectreport(report=rep)
INTERNALERROR>   File "/usr/local/lib/python2.7/dist-packages/_pytest/main.py", line 157, in call_matching_hooks
INTERNALERROR>     return hookmethod.pcall(plugins, **kwargs)
INTERNALERROR>   File "/usr/local/lib/python2.7/dist-packages/_pytest/core.py", line 426, in pcall
INTERNALERROR>     return self._docall(methods, kwargs)
INTERNALERROR>   File "/usr/local/lib/python2.7/dist-packages/_pytest/core.py", line 433, in _docall
INTERNALERROR>     res = mc.execute()
INTERNALERROR>   File "/usr/local/lib/python2.7/dist-packages/_pytest/core.py", line 351, in execute
INTERNALERROR>     res = method(**kwargs)
INTERNALERROR>   File "/usr/local/lib/python2.7/dist-packages/_pytest/resultlog.py", line 88, in pytest_collectreport
INTERNALERROR>     longrepr = str(report.longrepr.reprcrash)
INTERNALERROR> AttributeError: CollectErrorRepr instance has no attribute 'reprcrash'

=========================== 1 error in 0.23 seconds ============================"""

    # Test for BUG 2.  The likely-fragile is in the function we call
    # here.
    if (looks_like_bug2(dirpath, log)):
        # When suspecting the bug, log it, inform the student (via the
        # returned tuple), and inform the admin (via email directly).

        log.info("Reached branch for suspected BUG 2")

        error_report = error_report_import_failed

        admin_errorreport = """
Suspect BUG 2 in subtest:__init__.py (import of file [with non-ASCII
characters?     ] fails).

This arose in directory {0:s}.  The following error report has been
included in an email to the student.\n\n""".format(dirpath) + error_report

        enqueue_outgoing_mails.send_text_message(
            conf.SysadminEmail,
            conf.ModuleEmailAddress,
            admin_errorreport,
            "Error importing student submission",
            html=False)

        tests.append((False, "import s.py", error_report))
        return tests, status

    # otherwise carry on -- this is the normal case.
    i = 0
    while i < n:
        log.info("i={}, n={}, parsing line '{}'".format(i, n, lines[i]))

        if len(lines[i]) == 0:
            raise ValueError, "Empty line [%d] in '%s' -- impossible" % (i,
                                                                         path)
        testname = lines[i][1:].strip()
        if lines[i][0] == '.':  #passed
            tests.append((True, testname, None))
            i = i + 1
        elif lines[i][0] == 'F':  #failed
            #Errors that have ocurred while executing any functions defined as 'def test_'
            #will be correctly reported in the pytest_log file. For example:
            #F 5NYr6E/test_tenfunc.py:test_log2
            #
            #If there is an error when the s.py file is imported, this will not be
            #explained in pytest_log (but in pytest.stdout). The corresponding location
            #of the error in pytest_log reads:
            #F _test-2009-09-23-23:00:04/test_lab1.py
            #
            #Note that in case 1, the failing function ('test_log2') is mentioned whereas
            #below that in case2, only the failing file is mentioned ('test_lab1').
            #Can use this to distinguish the cases:

            #Need to catch this case
            if lines[i].strip().endswith(
                    ".py"):  #if this ends with .py, then the import has failed
                import_error= '\nThere was an error when importing your file:\n'\
                              +open(os.path.join(dirpath,'pytest.stdout')).read()

                import_error += error_report_import_failed

                log.debug(
                    "Have identified an import error: import_error message='{}'"
                    .format(import_error))

            else:
                import_error = ""  #import_error = "Not an import error, lines[i]='%s'" % lines[i]

            i = i + 1
            errorreport = ""
            #Now normal parsing of the next (indented) lines:
            while lines[i][0] == " ":
                errorreport += lines[i][1:]
                i = i + 1
                if i == n:
                    break
                if len(lines[i]) == 0:
                    raise ValueError, "Empty line [%d] in '%s' -- impossible" % (
                        i, path)
            errorreport += import_error
            log.debug("Final error report for {} is '{}'".format(
                testname, errorreport))
            tests.append((False, testname, errorreport))

        else:  # This line has length but does not start with '.' or
            # 'F'.  Should not happen, warn admin and student.
            admin_errorreport = """Error in parse_pytest_report.  We encountered a line in pytest_log
which begins neither with '.' nor 'F'.  This may be a bug.  The
directory in which this error arose is {0:s} and the contents of the
pytest_log file follow.""".format(dirpath)
            admin_errorreport += "\n\n"
            with open(path, 'r') as fh:
                admin_errorreport += fh.read()

            enqueue_outgoing_mails.send_text_message(
                conf.SysadminEmail,
                conf.ModuleEmailAddress,
                admin_errorreport,
                "Error parsing pytest_log",
                html=False)

            error_report = """An error has occurred in parsing the log file pytest_log.  This may be
due to issues with your submission, or it may be because of a bug in
the testing system.  The laboratory leader has been informed.

Please ensure that you can run your file (either by pressing F5 in
IDLE, or running "python MYFILE.py") without getting any errors,
*BEFORE* submitting it.

If you are submitting C code, please ensure it compiles without
errors before submitting.

If the above makes no sense (either because you don't understand
it, or because you do understand but find this does not match
reality), then please contact a demonstrator or the laboratory
leader about this."""

            tests.append((False, "Error parsing pytest_log", error_report))
            return tests, status

    log.debug(
        "leaving parse_pytest_report normally. Data: \ntests={}".format(tests))
    log.debug("status={}".format(status))

    return tests, status
def send_mail( msg, debug = 0 ):
    """Sends a mail (in email-message format). Expect msg to be of type
    email.Message.Message(), retuns os.EX_OK on success, None or an os.*
    error code on error.
    """

    # turn this off for off-line testing
    network = 1

    mailfrom = msg["From"]

    # For multiple recipients:
    # msg['To'] ought to look like 'recipient1, recipient2'
    # whilst smtp.sendmail() should get a list ['recipient1', 'recipient2']
    if msg["To"].count(",") == 0:
        # single recipient
        mailto = msg["To"]
    else:
        mailto = [recip.replace(" ","") for recip in msg["To"].split(",")]

    assert mailfrom, "No from-addr given: %s" % repr(mailfrom)
    assert mailto, "No to-addr given: %s" % repr(mailto)

    log_global.info("send_mail(): email from %s to %s" %
                    (repr(mailfrom),repr(mailto)))

    log_global.debug("send_mail(): Beginning of content is\n%s" % msg.as_string()[0:1000*80])
    if debug:
          print "Beginning of content is",msg.as_string()[0:1000*80]

    log_global.info("sending email from %s to %s" % (repr(mailfrom),repr(mailto)))

    if not(network):
        log_global.warn("Not sending email (not in network mode)")
        print "==BEGIN Sent Email========================="
        print msg.as_string()[0:1*80]
        print "==END Sent Email==========================="
        return os.EX_OK
    else:
        log_global.info("(deliver email to %s via smtp to %s)" % (repr(mailto),repr(conf.smtp)))
        import socket

        try:
            smtp = smtplib.SMTP(conf.smtp)
            smtp.set_debuglevel(0) #put in 1 for messages to stdout/stderr
            smtp.sendmail(mailfrom, mailto, msg.as_string())
            smtp.quit()
            log_global.debug("finished sending email to %s" % repr(mailto))
            record_smtp_socket_success()
            return os.EX_OK

        except socket.error,err_msg:
            # This arises most commonly when the SMTP server is
            # overloaded or unavailable - our main goal is to keep
            # re-trying this message (so return None to keep it in the
            # queue), and notify admin from time to time, as per
            # config settings.
            log_global.warn("smtp delivery failed, will retry on next queue run, error: {0:s}".format(err_msg))
            process_smtp_socket_error(err_msg)

            return None

        except smtplib.SMTPRecipientsRefused,err_msg:
            # Mostly occurs if we mistakenly reply to spam.  It is not
            # desired to stop the system in this case, so we inform
            # the administrator, including the message that we could
            # not send to its original recipient, and carry on.  We
            # return EX_OK in order that the undeliverable message
            # be removed from the queue of messages for sending.
            undelivered_string = msg.as_string()
            text = ("WARNING: could not deliver email.\n"
                    "The rest of this message contains the mail that was undeliverable,\n"
                    "followed by the error log and exception error message.\n\n\n")
            text += "\n" + 40*"=" + "\n"
            text += undelivered_string + "\n" + 40*"=" + "\n"
            ins,outs = os.popen4('tail -n 100 '+conf.Logfile)
            text += outs.read()+'\n'+'Error message is:\n'+str(err_msg)
            subject = "WARNING: Malfunction in %s at %s (probably spam) (could not deliver email but will carry on, might have been spam)" % (conf.ModulecodeSubjectLine,time.asctime())
            enqueue_outgoing_mails.send_text_message( conf.SysadminEmail, conf.ModuleEmailAddress,text, subject)
            log_global.info("Emailed sysadmin")
            log_global.warning("Failed to send email to %s" % repr(mailto))

            record_smtp_socket_success()

            return os.EX_OK 
        log_global.info("Other process running, quitting")
    else: #all well

        log_global.debug("About to read queue")

        if live:
            try:
                process_queue()
                unlock_semaphore(lock)
            except:
                log_global.exception("Something went wrong (caught globally)")

                log_global.critical("Preparing email to sysadmin (%s)" % repr(conf.SysadminEmail))
                ins,outs = os.popen4('tail -n 100 '+conf.outgoingmail_logfile)
                text = outs.read()
                subject = "URGENT: Malfunction in process_outgoing_mails %s at %s !!!" % (conf.ModulecodeSubjectLine,time.asctime())
                enqueue_outgoing_mails.send_text_message( conf.SysadminEmail, conf.ModuleEmailAddress,text, subject)
                log_global.info("Leaving now (not removing lockfile).")
                raise
        else:
            process_queue()
            unlock_semaphore(lock)

    import time
    f=open(conf.outgoingmail_pulsefile,'w')
    data = {'now-secs':time.time(),'now-ascii':time.ctime(),'module':conf.ModulecodeSubjectLine,
            'what':"process-outgoingmail"}
    f.write("%s" % repr(data))
    f.close()
    log_global.debug("About to leave, updated pulse.")
Beispiel #14
0
This submission attempt is not a valid submission -- please
improve code and resubmit.



More detailed analysis
----------------------

Below, we provide the outcome (ok/failed) of those tests that have been run
and terminated. The problem of non-termination is likely to be with the next
test to be run.

""" +post_test_analysis.summary_text(report)+\
               """\n\nAsk a demonstrator for help if this does not make sense to you.\n"""
        enqueue_outgoing_mails.send_text_message(job['email'],
                                                 conf.ModuleEmailAddress, text,
                                                 subject)

        admtext = """Unterminated code detected in job id {0:d}
(student lab directory was {1:s}).

Student {2:s} ({3:s}) was sent the following email.

""".format(job['id'], student_lab_dir, job['real_name'], job['login'])

        admtext = admtext + text
        admsubject = subject + " (" + job['login'] + " " + job[
            'real_name'] + ")"

        enqueue_outgoing_mails.send_text_message(conf.SysadminEmail,
                                                 conf.ModuleEmailAddress,