#!/usr/bin/env python # -*- coding: utf-8 -*- import unittest import re import sys import glob import helper from mockable_test_result import MockableTestResult from runner import path_to_enlightenment from libs.colorama import init, Fore, Style init() # init colorama class Sensei(MockableTestResult): def __init__(self, stream): unittest.TestResult.__init__(self) self.stream = stream self.prevTestClassName = None self.tests = path_to_enlightenment.koans() self.pass_count = 0 self.lesson_pass_count = 0 self.all_lessons = None def startTest(self, test): MockableTestResult.startTest(self, test) if helper.cls_name(test) != self.prevTestClassName: self.prevTestClassName = helper.cls_name(test) if not self.failures: self.stream.writeln() self.stream.writeln("{0}{1}Thinking {2}".format( Fore.RESET, Style.NORMAL, helper.cls_name(test))) if helper.cls_name(test) != 'AboutAsserts': self.lesson_pass_count += 1 def addSuccess(self, test): if self.passesCount(): MockableTestResult.addSuccess(self, test) self.stream.writeln( \ " {0}{1}{2} has expanded your awareness.{3}{4}" \ .format(Fore.GREEN, Style.BRIGHT, test._testMethodName, \ Fore.RESET, Style.NORMAL)) self.pass_count += 1 def addError(self, test, err): # Having 1 list for errors and 1 list for failures would mess with # the error sequence self.addFailure(test, err) def passesCount(self): return not (self.failures and helper.cls_name(self.failures[0][0]) != self.prevTestClassName) def addFailure(self, test, err): MockableTestResult.addFailure(self, test, err) def sortFailures(self, testClassName): table = list() for test, err in self.failures: if helper.cls_name(test) == testClassName: m = re.search("(?<= line )\d+" ,err) if m: tup = (int(m.group(0)), test, err) table.append(tup) if table: return sorted(table) else: return None def firstFailure(self): if not self.failures: return None table = self.sortFailures(helper.cls_name(self.failures[0][0])) if table: return (table[0][1], table[0][2]) else: return None def learn(self): self.errorReport() self.stream.writeln("") self.stream.writeln("") self.stream.writeln(self.report_progress()) self.stream.writeln("") self.stream.writeln(self.say_something_zenlike()) if self.failures: sys.exit(-1) self.stream.writeln( "\n{0}**************************************************" \ .format(Fore.RESET)) self.stream.writeln("\n{0}That was the last one, well done!" \ .format(Fore.MAGENTA)) self.stream.writeln( "\nIf you want more, take a look at about_extra_credit_task.py") def errorReport(self): problem = self.firstFailure() if not problem: return test, err = problem self.stream.writeln(" {0}{1}{2} has damaged your " "karma.".format(Fore.RED, Style.BRIGHT, test._testMethodName)) self.stream.writeln("\n{0}{1}You have not yet reached enlightenment ..." \ .format(Fore.RESET, Style.NORMAL)) self.stream.writeln("{0}{1}{2}".format(Fore.RED, \ Style.BRIGHT, self.scrapeAssertionError(err))) self.stream.writeln("") self.stream.writeln("{0}{1}Please meditate on the following code:" \ .format(Fore.RESET, Style.NORMAL)) self.stream.writeln("{0}{1}{2}{3}{4}".format(Fore.YELLOW, Style.BRIGHT, \ self.scrapeInterestingStackDump(err), Fore.RESET, Style.NORMAL)) def scrapeAssertionError(self, err): if not err: return "" error_text = "" count = 0 for line in err.splitlines(): m = re.search("^[^^ ].*$",line) if m and m.group(0): count+=1 if count>1: error_text += (" " + line.strip()).rstrip() + '\n' return error_text.strip('\n') def scrapeInterestingStackDump(self, err): if not err: return "" lines = err.splitlines() sep = '@@@@@SEP@@@@@' scrape = "" for line in lines: m = re.search("^ File .*$",line) if m and m.group(0): scrape += '\n' + line m = re.search("^ \w(\w)+.*$",line) if m and m.group(0): scrape += sep + line lines = scrape.splitlines() scrape = "" for line in lines: m = re.search("^.*[/\\\\]koans[/\\\\].*$",line) if m and m.group(0): scrape += line + '\n' return scrape.replace(sep, '\n').strip('\n') def report_progress(self): return ("You are now {0}/{1} koans and {2}/{3} lessons away from " \ "reaching enlightenment".format(self.pass_count, self.total_koans(), self.lesson_pass_count, self.total_lessons())) # Hat's tip to Tim Peters for the zen statements from The Zen # of Python (http://www.python.org/dev/peps/pep-0020/) # # Also a hat's tip to Ara T. Howard for the zen statements from his # metakoans Ruby Quiz (http://rubyquiz.com/quiz67.html) and # Edgecase's later permatation in the Ruby Koans def say_something_zenlike(self): if self.failures: turn = self.pass_count % 37 zenness = ""; if turn == 0: zenness = "Beautiful is better than ugly." elif turn == 1 or turn == 2: zenness = "Explicit is better than implicit." elif turn == 3 or turn == 4: zenness = "Simple is better than complex." elif turn == 5 or turn == 6: zenness = "Complex is better than complicated." elif turn == 7 or turn == 8: zenness = "Flat is better than nested." elif turn == 9 or turn == 10: zenness = "Sparse is better than dense." elif turn == 11 or turn == 12: zenness = "Readability counts." elif turn == 13 or turn == 14: zenness = "Special cases aren't special enough to " \ "break the rules." elif turn == 15 or turn == 16: zenness = "Although practicality beats purity." elif turn == 17 or turn == 18: zenness = "Errors should never pass silently." elif turn == 19 or turn == 20: zenness = "Unless explicitly silenced." elif turn == 21 or turn == 22: zenness = "In the face of ambiguity, refuse the " \ "temptation to guess." elif turn == 23 or turn == 24: zenness = "There should be one-- and preferably only " \ "one --obvious way to do it." elif turn == 25 or turn == 26: zenness = "Although that way may not be obvious at " \ "first unless you're Dutch." elif turn == 27 or turn == 28: zenness = "Now is better than never." elif turn == 29 or turn == 30: zenness = "Although never is often better than right " \ "now." elif turn == 31 or turn == 32: zenness = "If the implementation is hard to explain, " \ "it's a bad idea." elif turn == 33 or turn == 34: zenness = "If the implementation is easy to explain, " \ "it may be a good idea." else: zenness = "Namespaces are one honking great idea -- " \ "let's do more of those!" return "{0}{1}{2}{3}".format(Fore.CYAN, zenness, Fore.RESET, Style.NORMAL); else: return "{0}Nobody ever expects the Spanish Inquisition." \ .format(Fore.CYAN) # Hopefully this will never ever happen! return "The temple in collapsing! Run!!!" def total_lessons(self): all_lessons = self.filter_all_lessons() if all_lessons: return len(all_lessons) else: return 0 def total_koans(self): return self.tests.countTestCases() def filter_all_lessons(self): if not self.all_lessons: self.all_lessons = glob.glob('koans/about*.py') self.all_lessons = filter(lambda filename: "about_extra_credit" not in filename, self.all_lessons) return self.all_lessons