# Solfege - free ear training software
# Copyright (C) 2007, 2008 Tom Cato Amundsen
# License is GPL, see file COPYING

import unittest
import time

from src.testlib import I18nSetup
import src.lessonfile
src.lessonfile._test_mode = True
from src.lessonfile import *
from src import dataparser
import mpd
from src import cfg
from mpd import mpdutils

class TestParser(unittest.TestCase):
    def test_bad_musictype(self):
        s = 'question {\n' \
            'music = music("c e g", "chor")\n' \
            '}'
        p = LessonfileCommon()
        try:
            p.parse_string(s)
        except src.dataparser.WrongArgumentCount, e:
            self.assertEquals('(line 1): question {\n'
                              '(line 2): music = music("c e g", "chor")\n'
                              '                  ^',
                              e.m_nonwrapped_text)
    def test_gettext(self):
        s = 'question {\n' \
            '  name = _("chord|m7")\n' \
            '  iname = _i("chord|m7")\n' \
            '}'
        p = LessonfileCommon()
        p.parse_string(s)
        self.assertEquals(p.m_questions[0].name, "chord|m7")
        self.assertEquals(p.m_questions[0].name.cval, "chord|m7")
        self.assertEquals(p.m_questions[0].iname, "m7")
        self.assertEquals(p.m_questions[0].iname.cval, "chord|m7")

class _Common(unittest.TestCase):
    def setUp(self):
        self.p = QuestionsLessonfile()
        self.p.parse_string("""
        header { title = "jojo" }
        question { music = chord("c e g") }
        """)

class TestChord(unittest.TestCase):
    def setUp(self):
        cfg.set_bool('config/override_default_instrument', False)
        self.p = QuestionsLessonfile()
        self.p.parse_string("""
        header { random_transpose = no }
        question { music = chord("c e g") }
        """)
        self.p.m_transpose = mpd.MusicalPitch.new_from_notename("d'")
        self.p._idx = 0
    def get_get_mpd_music_string(self):
        self.assertEquals(self.p.m_questions[0]['music'].get_mpd_music_string(self.p),
            r"\staff{ <c e g> }")
        self.p.header.random_transpose = True
        self.assertEquals(self.p.m_questions[0]['music'].get_mpd_music_string(self.p),
            r"\staff\transpose d'{ <c e g> }")
    def test_get_music_as_notename_list(self):
        self.assertEquals(self.p.m_questions[0]['music'].get_music_as_notename_list(self.p), ['c', 'e', 'g'])
        self.p.header.random_transpose = True
        self.p.m_transpose = mpd.MusicalPitch.new_from_notename("d'")
        self.assertEquals(self.p.m_questions[0]['music'].get_music_as_notename_list(self.p), ['d', 'fis', 'a'])
    def test_get_lilypond_code(self):
        self.assertEquals(self.p.m_questions[0]['music'].get_lilypond_code(self.p), "\\score{  \\transpose c' d'{ <c e g> } \\layout {   ragged-last = ##t   \\context { \\Staff \\remove \"Time_signature_engraver\" }  }}")
    def test_get_lilypond_code_first_note(self):
        self.assertEquals(self.p.m_questions[0]['music'].get_lilypond_code_first_note(self.p), r"\transpose c' d'{ c }")
    def test_play(self):
        question = self.p.m_questions[0]
        question['music'].play(self.p, question)
        self.assertEquals(soundcard.synth.flush_testdata(),
            "t60/4 p0:0 n48 n52 n55 d1/4 o48 o52 o55")
    def test_play_arpeggio(self):
        question = self.p.m_questions[0]
        question['music'].play_arpeggio(self.p, question)
        self.assertEquals(soundcard.synth.flush_testdata(),
            "t180/4 p0:0 n48 d1/4 o48 n52 d1/4 o52 n55 d1/4 o55")
        self.p.header.random_transpose = True
        question['music'].play_arpeggio(self.p, question)
        self.assertEquals(soundcard.synth.flush_testdata(),
            "t180/4 p0:0 n50 d1/4 o50 n54 d1/4 o54 n57 d1/4 o57")

class TestMusic(unittest.TestCase):
    def setUp(self):
        cfg.set_bool('config/override_default_instrument', False)
        self.p = QuestionsLessonfile()
        self.p.parse_string("""
        header { random_transpose = no }
        question { music = music("\staff{c''}\staff{e'}\staff{c}") }
        """)
        self.p.m_transpose = mpd.MusicalPitch.new_from_notename("d'")
        self.p._idx = 0
    def test_play(self):
        question = self.p.m_questions[0]
        question['music'].play(self.p, question)
        self.assertEquals(soundcard.synth.flush_testdata(1),
            "t60/4 p0:0 n0:48 n0:64 n0:72 d1/4 o48 o64 o72")
        cfg.set_bool('config/override_default_instrument', True)
        question['music'].play(self.p, question)
        self.assertEquals(soundcard.synth.flush_testdata(1),
            "t60/4 p0:3 p1:2 p2:1 n0:72 n1:64 n2:48 d1/4 o48 o64 o72")
    def test_play_slowly(self):
        question = self.p.m_questions[0]
        #test.py TestMusic viser at det ikke blir generert riktig instrument
        #for ovelser som progressions-2 (harmonicprogressiondictation)
        question['music'].play_slowly(self.p, question)
        self.assertEquals(soundcard.synth.flush_testdata(1),
            "t30/4 p0:0 n0:48 n0:64 n0:72 d1/4 o48 o64 o72")
        cfg.set_bool('config/override_default_instrument', True)
        question['music'].play_slowly(self.p, question)
        self.assertEquals(soundcard.synth.flush_testdata(1),
            "t30/4 p0:3 p1:2 p2:1 n0:72 n1:64 n2:48 d1/4 o48 o64 o72")

class TestVoice(unittest.TestCase):
    def setUp(self):
        cfg.set_bool('config/override_default_instrument', False)
        self.p = QuestionsLessonfile()
        self.p.parse_string("""
        header { random_transpose = no }
        question { music = voice("c d e") }
        """)
        self.p.m_transpose = mpd.MusicalPitch.new_from_notename("d'")
        self.p._idx = 0
    def test_get_mpd_music_string(self):
        self.assertEquals(self.p.m_questions[0]['music'].get_mpd_music_string(self.p),
            "\\staff{\nc d e\n}")
        self.p.header.random_transpose = True
        self.assertEquals(self.p.m_questions[0]['music'].get_mpd_music_string(self.p),
            "\\staff\\transpose d'{\nc d e\n}")
    def test_get_lilypond_code(self):
        self.assertEquals(self.p.m_questions[0]['music'].get_lilypond_code(self.p), r"\transpose c' d'{ c d e }")
    def test_get_lilypond_code_first_note(self):
        self.assertEquals(self.p.m_questions[0]['music'].get_lilypond_code_first_note(self.p), r"\score{ \new Staff<<   \new Voice\transpose c' d'{ \cadenzaOn c }  \new Voice{ \hideNotes c d e }  >> \layout { ragged-last = ##t } }")
    def test_play(self):
        question = self.p.m_questions[0]
        question['music'].play(self.p, question)
        self.assertEquals(soundcard.synth.flush_testdata(),
            "t60/4 p0:0 n48 d1/4 o48 n50 d1/4 o50 n52 d1/4 o52")
    def test_play_slowly(self):
        question = self.p.m_questions[0]
        question['music'].play_slowly(self.p, question)
        self.assertEquals(soundcard.synth.flush_testdata(),
            "t30/4 p0:0 n48 d1/4 o48 n50 d1/4 o50 n52 d1/4 o52")
    def test_findfirst(self):
        for p1, p2, s, n in (
            (0, 1, "c d e", "c"),
            (1, 4, "<c'' e>", "c''"),
            (2, 5, "< c'' e>", "c''"),
            (2, 5, "\n\nERR", "ERR"),
            (1, 4, " c,,16", "c,,"),
            (3, 4, " \n c d e", "c"),
            (11, 12, r"\clef bass c d e", "c"),
            (23, 26, r"\clef violin \time 3/8 fis", "fis"),
            (17, 18, r'\clef "violin_8" d', "d"),
            (25, 27, "\\key g \\major \\time 2/4\n d'8 | [g g]", "d'"),
            (1, 2, "\nc", "c"),
            ):
            x = Rvoice(s)
            self.assertEquals((p1, p2), mpdutils.find_possible_first_note(x.m_musicdata), "String failed: %s: %s" % (s, mpdutils.find_possible_first_note(x.m_musicdata)))
            self.assertEquals(s[p1:p2], n)

class TestErrorHandling(unittest.TestCase):
    def setUp(self):
        cfg.set_bool('config/override_default_instrument', False)
    def _dotest(self, musictypes, bad_code):
        for t in musictypes:
            for code, err_context in bad_code:
                self.p = QuestionsLessonfile()
                self.p.parse_string('''
                header { random_transpose = no }
                question { music = %s("%s") }
                ''' % (t, code))
                self.p.m_transpose = mpd.MusicalPitch.new_from_notename("d'")
                self.p._idx = 0
                question = self.p.m_questions[0]
                try:
                    self.p.play_question(question)
                except MusicObjectException, e:
                    self.assertEquals(err_context, str(e))
                except mpd.MpdException, e:
                    ec = question.music.get_err_context(e, self.p)
                    self.assertEquals(err_context, ec, "Music failed with musictype %s:\n%s !=\n%s" % (t, err_context, ec))
                else:
                    assert False, "No error found in code %s" % code
    def test_1(self):
        self._dotest(("chord", "voice", "rvoice", "rhythm", "percussion", "rhythm"),
                   (("c d e ERR f",
                     "c d e ERR f\n"
                     "      ^^^"),
                    ("c d e f\ng ERR a",
                    "g ERR a\n"
                     "  ^^^"),
                    ("c d XX e f\ng ERR a",
                     "c d XX e f\n"
                     "    ^^"),
                    ("ERR",
                     "ERR\n"
                     "^^^"),
                    ("4ERR",
                     "4ERR\n"
                     "^"),
                  ))
    def test_satb(self):
        self._dotest(("satb",),
            (("c'' | ERR | g | c",
              "c'' | ERR | g | c\n"
              "     ^^^^^"),
             ("c''|g'|ERR| c",
              "c''|g'|ERR| c\n"
              "       ^^^"),
             ("c'' |e' d'|\n ERR |c",
              "The music code from the lesson file has been modified by\n"
              "removing the new-line characters. This to more easily show\n"
              "where the error occured. Satb music should not contain music\n"
              "characters.\n"
              "c'' |e' d'| ERR |c\n"
              "           ^^^^^"),
             ("|||",
              "Satb music does not allow an empty voice"),
             ("{ | g",
              "Satb music should be divided into 4 parts by the '|' character",),
              ))
    def test_music(self):
        self._dotest(("music",),
            (("\\staff{ c' d ERR e }",
              "\\staff{ c' d ERR e }\n"
              "             ^^^"),
             ("\\staff{ c4. c. }",
              "\\staff{ c4. c. }\n"
              "            ^^"),
             ("\\addvoice{ jojo ",
              "\\addvoice{ jojo \n"
              "^^^^^^^^^"),
             ("\\staff { c4 c4. c. }",
              "\\staff { c4 c4. c. }\n"
              "                ^^"),
             ("\\staff { c4\n"
              "ERR }",
              "ERR }\n"
              "^^^"),
             ("\\staff{ \\clef ERROR c",
              "\\staff{ \\clef ERROR c\n"
                "        ^^^^^^^^^^^"),
            ))


class TestRvoice(unittest.TestCase):
    def setUp(self):
        cfg.set_bool('config/override_default_instrument', False)
        self.p = QuestionsLessonfile()
        self.p.parse_string("""
        header { random_transpose = no }
        question { music = rvoice("c' d b c") }
        question { music = rvoice("[c8' d] e16") }
        """)
        self.p.m_transpose = mpd.MusicalPitch.new_from_notename("d'")
        self.p._idx = 0
    def test_get_mpd_music_string(self):
        self.assertEquals(self.p.m_questions[0]['music'].get_mpd_music_string(self.p),
            "\\staff\\relative c'{\nc d b c\n}")
        self.p.header.random_transpose = True
        self.assertEquals(self.p.m_questions[0]['music'].get_mpd_music_string(self.p),
            "\\staff\\transpose d'\\relative c'{\nc d b c\n}")
    def test_play(self):
        question = self.p.m_questions[0]
        question['music'].play(self.p, question)
        self.assertEquals(soundcard.synth.flush_testdata(),
            "t60/4 p0:0 n60 d1/4 o60 n62 d1/4 o62 n59 d1/4 o59 n60 d1/4 o60")
    def test_play_slowly(self):
        question = self.p.m_questions[0]
        question['music'].play_slowly(self.p, question)
        self.assertEquals(soundcard.synth.flush_testdata(),
            "t30/4 p0:0 n60 d1/4 o60 n62 d1/4 o62 n59 d1/4 o59 n60 d1/4 o60")
    def test_get_lilypond_code(self):
        self.assertEquals(self.p.m_questions[0]['music'].get_lilypond_code(self.p), r"\transpose c' d'\relative c{ c' d b c }")
    def test_get_lilypond_code_first_note(self):
        self.assertEquals(self.p.m_questions[0]['music'].get_lilypond_code_first_note(self.p), r"\score{ \new Staff<<   \new Voice\transpose c' d'\relative c{ \cadenzaOn c' }  \new Voice{ \hideNotes c' d b c }  >>  \layout { ragged-last = ##t } }")
    def test_with_bream_first_note(self):
        self.assertEquals(self.p.m_questions[1].music.get_mpd_music_string(self.p), "\\staff\\relative c{\n[c8' d] e16\n}")

class TestSatb(unittest.TestCase):
    def setUp(self):
        cfg.set_bool('config/override_default_instrument', False)
        self.p = QuestionsLessonfile()
        self.p.parse_string("""
        header { random_transpose = no }
        question { music = satb("c''| e'| g | c") }
        question { key = "aes \\major" music = satb("c''|as'|es'|as") }
        question { music = satb("c''| g' e'| g | c") }
        """)
        self.p.m_transpose = mpd.MusicalPitch.new_from_notename("d'")
        self.p._idx = 0
    def test_get_mpd_music_string(self):
        self.assertEquals(self.p.m_questions[0]['music'].get_mpd_music_string(self.p),
            "\\staff{ \\key c \\major\\stemUp <c''> }\n" \
            "\\addvoice{ \\stemDown <e'> }\n" \
            "\\staff{ \\key c \\major\\clef bass \\stemUp <g>}\n" \
            "\\addvoice{ \\stemDown <c>}")
        # FIXME Satb only works if the music object is the current selected
        # question. Can we get around this?
        self.p._idx  = 1
        self.assertEquals(self.p.m_questions[1]['music'].get_mpd_music_string(self.p),
            "\\staff{ \\key aes \\major\\stemUp <c''> }\n" \
            "\\addvoice{ \\stemDown <as'> }\n" \
            "\\staff{ \\key aes \\major\\clef bass \\stemUp <es'>}\n" \
            "\\addvoice{ \\stemDown <as>}")
    def test_play_arpeggio(self):
        question = self.p.m_questions[0]
        question['music'].play_arpeggio(self.p, question)
        self.assertEquals(soundcard.synth.flush_testdata(),
            "t60/4 p0:0 n60 d1/4 o60 n52 d1/4 o52 n55 d1/4 o55 n48 d1/4 o48")
        question = self.p.m_questions[2]
        question['music'].play_arpeggio(self.p, question)
        self.assertEquals(soundcard.synth.flush_testdata(),
            "t60/4 p0:0 n60 d1/4 o60 n55 d1/4 o55 n52 d1/4 o52 n55 d1/4 o55 n48 d1/4 o48")
        # Then with transposition
        self.p.header.random_transpose = True
        question['music'].play_arpeggio(self.p, question)
        self.assertEquals(soundcard.synth.flush_testdata(),
            "t60/4 p0:0 n62 d1/4 o62 n57 d1/4 o57 n54 d1/4 o54 n57 d1/4 o57 n50 d1/4 o50")

class TestRhythm(unittest.TestCase):
    def setUp(self):
        cfg.set_bool('config/override_default_instrument', False)
        self.p = QuestionsLessonfile()
        self.p.parse_string("""
        question { music = rhythm("d c8") }
        """)
        self.p._idx = 0
    def test_play(self):
        question = self.p.m_questions[0]
        question['music'].play(self.p, question)
        self.assertEquals(soundcard.synth.flush_testdata(),
            "t60/4 P80 d1/4 o80 P37 d1/8 o37")

class TestPercussion(unittest.TestCase):
    def setUp(self):
        cfg.set_bool('config/override_default_instrument', False)
        self.p = QuestionsLessonfile()
        self.p.parse_string("""
        question { music = percussion("e16 f g4") }
        """)
        self.p._idx = 0
    def test_play(self):
        question = self.p.m_questions[0]
        question['music'].play(self.p, question)
        self.assertEquals(soundcard.synth.flush_testdata(),
            "t60/4 P52 d1/16 o52 P53 d1/16 o53 P55 d1/4 o55")


class TestWavfile(unittest.TestCase):
    def setUp(self):
        self.p = QuestionsLessonfile()
        self.p.parse_string("""
        question { music = wavfile("lesson-files/share/fifth-small-293.33.wav") }
        """)
        self.p._idx = 0
    def test_play(self):
        question = self.p.m_questions[0]
        if cfg.get_bool('testing/may_play_sound'):
            question['music'].play(self.p, question)
            time.sleep(3)
    def test_get_mpd_music_string(self):
        question = self.p.m_questions[0]
        self.assertEquals(question['music'].get_mpd_music_string(self.p),
                          "Wavfile:lesson-files/share/fifth-small-293.33.wav")
        self.assertEquals(question.music.m_musicdata,
                          "lesson-files/share/fifth-small-293.33.wav")
        self.assertTrue(isinstance(question.music, Wavfile))

class TestMidifile(unittest.TestCase):
    def setUp(self):
        self.p = QuestionsLessonfile()
        self.p.parse_string("""
        question { music = midifile("lesson-files/share/fanfare.midi") }
        """)
        self.p._idx = 0
    def test_play(self):
        question = self.p.m_questions[0]
        if cfg.get_bool('testing/may_play_sound'):
            question['music'].play(self.p, question)
            time.sleep(3)
    def test_get_mpd_music_string(self):
        question = self.p.m_questions[0]
        self.assertEquals(question['music'].get_mpd_music_string(self.p),
                          "Midifile:lesson-files/share/fanfare.midi")
        self.assertEquals(question.music.m_musicdata,
                          "lesson-files/share/fanfare.midi")
        self.assertTrue(isinstance(question.music, Midifile))


class TestCmdline(unittest.TestCase):
    def setUp(self):
        self.p = QuestionsLessonfile()
        self.p.parse_string("""
        question { music = cmdline("/usr/bin/play lesson-files/share/fifth-pure-220.00.wav") }
        """)
        self.p._idx = 0
    def test_play(self):
        question = self.p.m_questions[0]
        if cfg.get_bool('testing/may_play_sound'):
            question['music'].play(self.p, question)
            time.sleep(3)

class TestLessonfileHeader(I18nSetup):
    def test_untranslated_variable(self):
        self.p = QuestionsLessonfile()
        self.p.parse_string("""
        header { untranslated = "jojo"
           translated = _("major")
        }
        question { } #dummy question to avoid NoQuestionsInFileException
        """)
        self.assertEquals(self.p.header['untranslated'], u"jojo")
        self.assertEquals(self.p.header['untranslated'].cval, u"jojo")
        self.assertEquals(self.p.header['translated'], u"dur")
        self.assertEquals(self.p.header['translated'].cval, u"major")
        self.assertEquals(self.p.header.untranslated, u"jojo")
        self.assertEquals(self.p.header.untranslated.cval, u"jojo")
        self.assertEquals(self.p.header.translated, u"dur")
        self.assertEquals(self.p.header.translated.cval, u"major")
    def test_infile_translations_discard(self):
        self.p = QuestionsLessonfile()
        self.p.parse_string("""
        header {
           title = "C-locale header"
           title[et] = "et-locale header"
        }
        question { } #dummy question to avoid NoQuestionsInFileException
        """)
        # This test is run with LANGUAGE='no', so the 'et' translation
        # is discarded
        self.assertEquals(self.p.header.title, "C-locale header")
        self.assertEquals(self.p.header.title.cval, "C-locale header")
class TestLessonfileQuestion(I18nSetup):
    def test_question_name(self):
        self.p = QuestionsLessonfile()
        self.p.parse_string("""
        question { name = "untranslated" }
        question { name = _("major") }
        question { # question has no name
        }
        """)
        self.assertEquals(self.p.m_questions[0].name, u"untranslated")
        self.assertEquals(self.p.m_questions[0].name.cval, u"untranslated")
        self.assertEquals(self.p.m_questions[1].name, u"dur")
        self.assertEquals(self.p.m_questions[1].name.cval, u"major")
        self.assertRaises(AttributeError, lambda : self.p.m_questions[2].name)
    def test_setattr(self):
        q = dataparser.Question()
        q.var = 1
        self.assertEquals(q.var, 1)
        self.assertEquals(q['var'], 1)


class TestLessonfileMisc(unittest.TestCase):
    def test_eq(self):
        c = Chord("a b c")
        d = Rvoice("a b c")
        e = Rvoice("a b c")
        f = Rvoice("c c c")
        self.assertNotEquals(c, d)
        self.assertNotEquals(d, e)
        self.assertNotEquals(d, f)
    def test_IdPropertyLessonfileFilter(self):
        s = """header {
                qprops = "name", "inversion"
                qprop_labels = _("Name"), _("Inversion")
            }
            question { name = "one" }
            question { name = "two" inversion=2 }
            """
        p = IdPropertyLessonfile()
        p.parse_string(s)
        # One question should be discarded because it does not have
        # the inversion variable
        self.assertEquals(len(p.m_discards), 1)
        self.assertEquals(len(p.m_questions), 1)
    def test_IdPropertyLessonfileFilter2(self):
        s = 'header { qprops = "name", "inversion" qprop_labels = "Name", "Inversion" }\n' \
            'question { name = "one" inversion=1} \n' \
            'question { name = "two" inversion=2 top=0 } '
        p = IdPropertyLessonfile()
        p.parse_string(s)
        # No questions are discaded.
        # top is not a property since it is not added to qprops, so
        # thats why the first question is not discarded.
        self.assertEquals(len(p.m_discards), 0)
        self.assertEquals(len(p.m_questions), 2)
    def test_IdPropertyLessonfile_qprops(self):
        s = 'header { qprops = "name", "inversion", "toptone" qprop_labels = "Name", "Inversion", "Toptone" }\n' \
            'question { name = "one" inversion=1} \n' \
            'question { name = "two" inversion=2 top=0 } '
        p = IdPropertyLessonfile()
        p.parse_string(s)
        # No questions are discarded, since they both contain the same
        # two properties, of the tree listed in qprops. The 'top' variable
        # in the second question does not affect what is or is not discarded
        # because is it not a property (not defined in qprops)
        self.assertEquals(len(p.m_questions), 2)
    def test_ChordLessonfileFilter(self):
        s = """header { }
            question { name = "one" }
            question { name = "two" inversion=2 }
            """
        p = ChordLessonfile()
        p.parse_string(s)
        # One question should be discarded because it does not have
        # the inversion variable
        self.assertEquals(len(p.m_questions), 1)
    def test_ChordLessonfileFilter2(self):
        s = 'header { qprops = "name", "inversion" qprop_labels = "Name", "Inversion" }\n' \
            'question { name = "one" inversion=1} \n' \
            'question { name = "two" inversion=2 top=0 } '
        p = ChordLessonfile()
        p.parse_string(s)
        # No questions are discaded.
        # top is not a property since it is not added to qprops, so
        # thats why the first question is not discarded.
        self.assertEquals(len(p.m_questions), 2)
    def test_IdProperty(self):
        """
        Once, a single item qprops did not work.
        """
        s = 'header { qprops = "name" qprop_labels = "NAME" } ' \
            'question { name = "major" music = chord("c e g") } '
        p = IdPropertyLessonfile()
        p.parse_string(s)
        self.assertEquals(p.header.qprops, [u'name'])


suite = unittest.makeSuite(TestParser)
suite.addTest(unittest.makeSuite(TestChord))
suite.addTest(unittest.makeSuite(TestMusic))
suite.addTest(unittest.makeSuite(TestVoice))
suite.addTest(unittest.makeSuite(TestRvoice))
suite.addTest(unittest.makeSuite(TestSatb))
suite.addTest(unittest.makeSuite(TestRhythm))
suite.addTest(unittest.makeSuite(TestPercussion))
suite.addTest(unittest.makeSuite(TestWavfile))
suite.addTest(unittest.makeSuite(TestMidifile))
suite.addTest(unittest.makeSuite(TestCmdline))
suite.addTest(unittest.makeSuite(TestLessonfileHeader))
suite.addTest(unittest.makeSuite(TestLessonfileQuestion))
suite.addTest(unittest.makeSuite(TestLessonfileMisc))
suite.addTest(unittest.makeSuite(TestErrorHandling))
