test.py 7.95 KB
Newer Older
Louis's avatar
Louis committed
1
2
3
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
4
# Copyright (C) 2015 -- Louis Paternault (spalax@gresille.org)
Louis's avatar
Louis committed
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA

"""Test of exercises.

This module gather tests from all exercises. Running:

    python -m unittest discover

does just as expected.
"""

Louis's avatar
Louis committed
29
import codecs
30
import gettext
31
import glob
Louis's avatar
Louis committed
32
import logging
33
import os
Louis's avatar
Louis committed
34
import random
Louis's avatar
Louis committed
35
36
import shutil
import tempfile
Louis's avatar
Louis committed
37
38
import unittest

39
40
41
# Quick and dirty definition of `_` as the identity function
gettext.install('pyromaths', unicode=1)

Louis's avatar
Louis committed
42
import pyromaths
43
from pyromaths.outils import System
44
from pyromaths.cli import PyromathsException
Louis's avatar
Louis committed
45
46
47
48

# Logging configuration
logging.basicConfig(level=logging.INFO)
LOGGER = logging.getLogger()
Louis's avatar
Louis committed
49

50
51
52
53
54
def load_tests(*__args, **__kwargs):
    """Return an `unittest.TestSuite` containing tests from all exercises."""
    tests = TestPerformer()
    return tests.as_unittest_suite([
        (exo, tests.get_tested_seeds(exo))
Louis's avatar
Louis committed
55
        for exo in tests.iter_names()
56
57
        ])

58
class ExerciseNotFound(PyromathsException):
59
    """Name of exercise cannot be found in known exercises."""
Louis's avatar
Louis committed
60
61

    def __init__(self, exercise):
62
        super(ExerciseNotFound, self).__init__()
Louis's avatar
Louis committed
63
        self.exercise = exercise
Louis's avatar
Louis committed
64

65
66
67
68
69
70
71
72
73
74
75
76
    def __str__(self):
        return "Exercise '{}' not found.".format(self.exercise)

def test_path(dirlevel, name, seed, choice):
    """Return the path of file containing expected test result."""
    return os.path.join(
        pyromaths.Values.data_dir(),
        'ex',
        dirlevel,
        'tests',
        "%s.%s.%s" % (name, seed, choice)
        )
Louis's avatar
Louis committed
77

78
79
def generate(exercise_list, openpdf=False, destname=None, pipe=None):
    """Generate exercise list as a pdf, and return the resulting pdf name.
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117

    :param openpdf: Open pdf at the end of compilation.
    :param str destname: Destination name (use ``None`` to use the default name).
    :param list pipe: List of commands executed on the tex file before compilation.
    """
    tempdir = tempfile.mkdtemp()

    old_dir = os.path.abspath(os.getcwd())
    System.creation({
        'creer_pdf': True,
        'creer_unpdf': True,
        'titre': u"Fiche de révisions",
        'corrige': True,
        'niveau': "test",
        'nom_fichier': u'test.tex',
        'chemin_fichier': tempdir,
        'fiche_exo': os.path.join(tempdir, 'exercises.tex'),
        'fiche_cor': os.path.join(tempdir, 'exercises-corrige.tex'),
        'datadir': pyromaths.Values.data_dir(),
        'configdir': pyromaths.Values.configdir(),
        'modele': 'pyromaths.tex',
        'liste_exos': exercise_list,
        'les_fiches': pyromaths.Values.lesfiches(),
        'openpdf': openpdf,
        'pipe': pipe,
    })
    os.chdir(old_dir)

    if destname:
        shutil.move(
            os.path.join(tempdir, 'exercises.pdf'),
            destname,
        )
    else:
        destname = os.path.join(tempdir, 'exercises.pdf')

    return destname

118
119
class TestExercise(object):
    """Test of an exercise"""
Louis's avatar
Louis committed
120

121
    def __init__(self, exercise, seed):
Louis's avatar
Louis committed
122
        self.exercise = exercise
123
        self.seed = seed
Louis's avatar
Louis committed
124

125
    def show(self):
126
127
        """Generate exercise, and display its result."""
        self.generate(openpdf=1)
Louis's avatar
Louis committed
128

129
130
131
132
    def get_exercise(self):
        """Return an instanciated exercise."""
        random.seed(self.seed)
        return self.exercise()
Louis's avatar
Louis committed
133

134
135
136
    def generate(self, openpdf=0):
        """Generate exercise"""
        return generate([self.get_exercise()], openpdf=openpdf)
Louis's avatar
Louis committed
137

138
139
140
141
142
143
144
145
    def test_path(self, name):
        """Return the path of the file containing expected results."""
        return test_path(
            self.exercise.dirlevel,
            self.exercise.name(),
            self.seed,
            name,
            )
Louis's avatar
Louis committed
146

147
148
149
150
151
152
153
154
155
156
    def write(self):
        """Write expected test results."""
        exo = self.get_exercise()
        with codecs.open(self.test_path("statement"), "w", "utf8") as statement:
            statement.write(u"\n".join(exo.tex_statement()))
        with codecs.open(self.test_path("answer"), "w", "utf8") as answer:
            answer.write(u"\n".join(exo.tex_answer()))

    def read(self, choice):
        """Read expected test result."""
157
158
        with codecs.open(self.test_path(choice), "r", "utf8") as result:
            return result.read()
159
160
161
162
163
164
165
166
167
168
169
170
171
172

    def remove(self):
        """Remove test"""
        os.remove(self.test_path("statement"))
        os.remove(self.test_path("answer"))

    def changed(self):
        """Return `True` iff exercise has changed."""
        exo = self.get_exercise()
        if "\n".join(exo.tex_statement()) != self.read('statement'):
            return True
        if "\n".join(exo.tex_answer()) != self.read('answer'):
            return True
        return False
Louis's avatar
Louis committed
173

174
175
class UnittestExercise(unittest.TestCase):
    """Test an exercise, with a particular seed."""
Louis's avatar
Louis committed
176

Louis's avatar
Louis committed
177
178
179
    maxDiff = None

    def __init__(self, exercise=None):
180
        super(UnittestExercise, self).__init__()
Louis's avatar
Louis committed
181
182
183
184
185
186
        self.exercise = exercise

    def shortDescription(self):
        if self.exercise is None:
            return super(UnittestExercise, self).shortDescription()
        else:
187
            return self.exercise.exercise.name()
Louis's avatar
Louis committed
188

189
190
    def runTest(self):
        """Perform test"""
Louis's avatar
Louis committed
191
        exo = self.exercise.get_exercise()
Louis's avatar
Louis committed
192

Louis's avatar
Louis committed
193
194
195
        self.assertEqual(
            u"\n".join(exo.tex_statement()),
            self.exercise.read('statement'),
196
197
            )

Louis's avatar
Louis committed
198
199
200
        self.assertEqual(
            u"\n".join(exo.tex_answer()),
            self.exercise.read('answer'),
201
202
203
204
205
206
207
208
209
210
            )

class TestPerformer(object):
    """Perform tests over every exercises"""

    def __init__(self):
        self.exercises = {}
        levels = pyromaths.ex.load_levels()
        for level in levels:
            for exercise in levels[level]:
211
                self.exercises[exercise.name()] = exercise
212

213
214
    def iter_names(self):
        """Iterate over exercise names."""
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
        return self.exercises.keys()

    def get(self, exercise, seed):
        """Return the `TestExercise` object corresponding to the arguments."""
        if exercise not in self.exercises:
            raise KeyError(exercise)
        return TestExercise(self.exercises[exercise], seed)

    def get_tested_seeds(self, exercise):
        """Return seeds that are tested for this exercise"""
        if exercise not in self.exercises:
            raise ExerciseNotFound(exercise)
        statement_seeds = [
            os.path.basename(path).split(".")[1]
            for path in glob.glob(test_path(
                self.exercises[exercise].dirlevel,
                self.exercises[exercise].name(),
                '*',
                'statement'
                ))
            ]
        for seed in statement_seeds:
            if os.path.exists(test_path(
                    self.exercises[exercise].dirlevel,
                    self.exercises[exercise].name(),
                    seed,
                    'answer',
                )):
                yield int(seed)

    def iter_missing(self):
        """Iterate over exercises that are not tested."""
        for exercise in self.exercises:
            if not list(self.get_tested_seeds(exercise)):
                yield exercise

    def as_unittest_suite(self, exercises):
        """Return the tests, as a `unittest.TestSuite`."""
        suite = unittest.TestSuite()
        for exercise, seeds in exercises:
            for seed in seeds:
Louis's avatar
Louis committed
256
                suite.addTest(UnittestExercise(exercise=self.get(exercise, seed)))
257
        return suite