Commit 7c6742b3 authored by Louis's avatar Louis
Browse files

Transformation de testexos.py en pyromaths-cli

* Renommage du binaire
* Suppression du binaire testexos.py : il faut maintenant appleer ``python -m pyromaths.cli``
* L'outil de test est maintenant une sous-commande de ``pyromaths-cli``
* Renommage de la commande `compile` en `generate`.
parent eaccac96
......@@ -18,7 +18,7 @@ Contenu :
pyromaths.classes.rst
pyromaths.outils.rst
pyromaths.ex.troisiemes.rst
pyromaths.tests
pyromaths.cli
......
Les tests des exercices
=======================
==============================
Interface en ligne de commande
==============================
L'outil ``testexo.py`` est conçu pour tester les exercises durant leur
conception, et plus tard, pour vérifier la non régression.
Pyromaths dispose d'une interface en ligne de commande : `pyromaths-cli`. Elle
peut-être utilisées dans des scripts, ou pour l'écriture de nouveaux exercices,
pour controler le rendu au fur et à mesure du développement.
.. contents::
:local:
Commandes disponibles
=====================
* Compilation d'un exercice (``python -m pyromaths.cli compile``) :
Compile un exercice, et crée le PDF correspondant (énoncé et solution) dans le dossier courant. Cette commande est utile pour tester un exercice en cours de rédaction, plutôt que de passer par l'interface graphique.
Il est également possible de fournir des commandes à exécuter sur les fichiers LaTeX avant leur compilation. Ceci est utile pour déceler des erreurs de code LaTeX. Par exemple, la commande ``python -m pyromaths.cli compile -p more EXERCICE`` affiche le code LaTeX dans `more` avant compilation; la commande ``python -m pyromaths.cli compile -p vim EXERCICE`` édite le fichier avec `vim` avant compilation. Ceci peut aussi être utilisé (en attendant une solution plus propre) pour garder une copie du fichier LaTeX en cas d'erreur de compilation, pour pouvoir l'analyser, ainsi que le log : ``python -m pyromaths.cli compile -p 'cp {} exercices.tex' EXERCICE``.
* Liste des identifiants des exercices disponibles (``python -m pyromaths.cli lsexos``) :
Affiche la liste des identifiants des exercices, pour retrouver facilement l'exercice en cours de travail. Plus d'informations sont données dans la partie :ref:`id_exos`.
* Test des exercices (``python -m pyromaths.cli test``) :
Voir la partie :ref:`pyrotests`.
.. _id_exos:
Description des exercices
=========================
Les exercices sur lesquels s'appliquent les commandes de ``pyromaths-cli`` sont
décrits comme ``quatriemes.exo_pythagore:4,6``, où :
* ``quatriemes`` est le niveau de l'exercice ;
* ``exo_pythagore`` est l'identifiant de l'exercice (le nom de la fonction ou
de la classe qui le définit) ;
* ``4,6`` sont les graines du générateur
aléatoire qui nous intéressent. Si les graines sont omises, suivant le cas,
soit tous les tests enregistrés sont considérés, soit la graine 0 est
utilisée.
La liste des exercices disponibles peut être obtenue avec la commande
``python -m pyromaths.cli lsexos``.
.. _pyrotests:
``python -m pyromaths.cli test``
================================
Cet outil permet de mettre en œuvre des tests de non régression pour les
exercices.
Principe
--------
......@@ -14,7 +58,7 @@ Les exercices de Pyromaths sont générés aléatoirement. Néanmoins, en fixant
graine (:meth:`random.seed`) du générateur aléatoire, il est possible de
générer deux fois de suite exactement le même exercice.
Le principe de ``testexo.py`` est le suivant : la personne concevant un
Le principe de ces tests est le suivant : la personne concevant un
exercice peut en valider un énoncé particulier. Plus tard, il sera possible de
vérifier que l'exercice produit toujours exactement la même sortie.
......@@ -31,44 +75,19 @@ Description rapide
Les commandes détaillées sont décrites ci-après. En voici une version
simplifiée.
* Création (``testexo.py create``), suppression des tests (``testexo.py remove``)
* Création (``python -m pyromaths.cli test create``), suppression des tests (``python -m pyromaths.cli test remove``)
* Mise à jour des tests (``testexp.py update``) :
* Mise à jour des tests (``python -m pyromaths.cli test update``) :
Effectue les tests, et propose de mettre à jour les tests qui ont changé. Utile si le code LaTeX généré a changé, mais l'exercice reste valide pour autant.
* Exécution des tests (``testexo.py check``) :
* Exécution des tests (``python -m pyromaths.cli test check``) :
Effectue les tests. Les tests sont aussi exécutés lorsqu'``unittest`` est appelé.
* Compilation d'un exercice (``testexo.py compile``) :
Compile un exercice, et crée le PDF correspondant (énoncé et solution) dans le dossier courant. Cette commande est utile pour tester un exercice en cours de rédaction, plutôt que de passer par l'interface graphique.
Il est également possible de fournir des commandes à exécuter sur les fichiers LaTeX avant leur compilation. Ceci est utile pour déceler des erreurs de code LaTeX. Par exemple, la commande ``testexo.py compile -p more EXERCICE`` affiche le code LaTeX dans `more` avant compilation; la commande ``testexo.py compile -p vim EXERCICE`` édite le fichier avec `vim` avant compilation. Ceci peut aussi être utilisé (en attendant une solution plus propre) pour garder une copie du fichier LaTeX en cas d'erreur de compilation, pour pouvoir l'analyser, ainsi que le log : ``testexo.py compile -p 'cp {} exercices.tex' EXERCICE``.
* Liste des identifiants des exercices disponibles (``testexo.py lsexos``) :
Affiche la liste des identifiants des exercices, pour retrouver facilement l'exercice en cours de travail.
Description des exercices
^^^^^^^^^^^^^^^^^^^^^^^^^
Les exercices sur lesquels s'appliquent les commandes de ``testexo.py`` sont
décrits comme ``quatriemes.exo_pythagore:4,6``, où :
* ``quatriemes`` est le niveau de l'exercice ;
* ``exo_pythagore`` est l'identifiant de l'exercice (le nom de la fonction ou
de la classe qui le définit) ;
* ``4,6`` sont les graines du générateur
aléatoire qui nous intéressent. Si les graines sont omises, suivant le cas,
soit tous les tests enregistrés sont considérés, soit la graine 0 est
utilisée.
La liste des exercices disponibles peut être obtenue avec la commande
``testexp.py lsexos``.
Description complète
^^^^^^^^^^^^^^^^^^^^
.. argparse::
:module: testexo
:module: pyromaths.cli.test.__main__
:func: argument_parser
:prog: testexo.py
:prog: pyromaths.cli.test.__main__.py
Version (unreleased)
* L'outils `testexo.py` a été renommé en `pyromaths.cli`.
* C'est désormais une interface en ligne de commandes à Pyromaths
* Il s'appelle avec `python -m pyromaths.cli`
Version 15.10
* Boîtes de dialogue natives sur OS X
* Compatibilité avec OS X 10.11 El Capitan
......
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 -- Louis Paternault (spalax@gresille.org)
#
# 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
"""Generic stuff for pyromaths command line interface"""
import argparse
class PyromathsException(Exception):
"""Generic exception for this module."""
pass
def exercise_argument(string=""):
"""Return the exercises matching ``string``.
:param str string: a string, option of one of `testexo` commands.
:rtype: dict
:return: A dictionary with exercises as keys, and sets of integers (seeds)
as values.
"""
splitted = string.split(":")
if len(splitted) == 1:
name = string
seeds = []
elif len(splitted) == 2:
name, seeds = string.split(":")
try:
seeds = [int(seed) for seed in seeds.split(",")]
except ValueError:
raise argparse.ArgumentTypeError("Seeds must be a comma separated list of integers.")
else:
raise argparse.ArgumentTypeError("Seeds must be a comma separated list of integers.")
return (name, seeds)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 -- Louis Paternault (spalax@gresille.org)
#
# 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
"""Pyromaths command line interface.
To display help:
> python -m pyromaths.cli --help
"""
import argparse
import gettext
import logging
import random
import sys
# Quick and dirty definition of `_` as the identity function
gettext.install('pyromaths', unicode=1)
from pyromaths.cli import exercise_argument, PyromathsException
from pyromaths.ex.test import TestPerformer, generate
# Logging configuration
logging.basicConfig(level=logging.INFO)
LOGGER = logging.getLogger()
VERSION = "0.1.0"
def argument_parser():
"""Return an argument parser"""
parser = argparse.ArgumentParser()
parser.add_argument(
'-v', '--version',
action='version',
version='%(prog)s {version}'.format(version=VERSION),
)
subparsers = parser.add_subparsers(title='Commands', dest='command')
subparsers.required = True
subparsers.dest = 'command'
# List exos
lsexos = subparsers.add_parser( # pylint: disable=unused-variable
'lsexos',
help=(
"List available exercises. Each line of the output can be used as "
"an argument to other commands."
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
# Generate
generate_parser = subparsers.add_parser(
'generate',
help='Generate some exercises.',
formatter_class=argparse.RawDescriptionHelpFormatter,
)
generate_parser.add_argument(
"exercise",
metavar='EXERCISE[:SEED[,SEED]]',
nargs='*', type=exercise_argument, default=None,
help='Exercises to generate. If empty, all exercises are generated.'
)
generate_parser.add_argument(
'-p', '--pipe',
nargs=1,
type=str,
action='append',
help=(
"Commands to run on the LaTeX file before compiling. String '{}' "
"is replaced by the file name; if not, it is appended at the end "
"of the string."
)
)
generate_parser.add_argument(
'-o', '--output',
type=str,
default='exercice.pdf',
help=(
"Output filename. Default is 'exercice.pdf'."
),
)
# Test
test = subparsers.add_parser(
'test',
help='Test exercices',
)
test.add_argument('args', nargs=argparse.REMAINDER)
return parser
def do_test(options):
"""Action for command line 'test'."""
from pyromaths.cli.test import __main__
sys.exit(__main__.main(options.args))
def do_generate(options):
"""Action for command line 'generate'."""
tests = TestPerformer()
if options.pipe is None:
options.pipe = []
else:
options.pipe = [item[0] for item in options.pipe]
exercise_list = []
for exercise, seeds in options.exercise:
if not seeds:
seeds = [random.randint(0, sys.maxint)]
for seed in seeds:
exercise_list.append(tests.get(exercise, seed).get_exercise())
generate(
exercise_list,
destname=options.output,
pipe=options.pipe,
)
def do_lsexos(options): # pylint: disable=unused-argument
"""Perform the `lsexos` command."""
tests = TestPerformer()
for exo_id in tests.iter_id():
print(exo_id) # pylint: disable=superfluous-parens
COMMANDS = {
"generate": do_generate,
"lsexos": do_lsexos,
"test": do_test,
}
def main():
"""Main function"""
options = argument_parser().parse_args(sys.argv[1:])
try:
COMMANDS[options.command](options)
except PyromathsException as error:
logging.error(error)
sys.exit(1)
if __name__ == "__main__":
main()
......@@ -21,20 +21,20 @@
To display help:
> testexo --help
> python -m pyromaths.cli.test --help
"""
import argparse
import gettext
import logging
import random
import sys
import unittest
# Quick and dirty definition of `_` as the identity function
gettext.install('pyromaths', unicode=1)
from pyromaths.ex.test import TestPerformer, TestException, compile
from pyromaths.cli import exercise_argument, PyromathsException
from pyromaths.ex.test import TestPerformer
# Logging configuration
logging.basicConfig(level=logging.INFO)
......@@ -59,29 +59,6 @@ def ask_confirm(message):
elif answer == 'n':
return False
def exercise_argument(string=""):
"""Return the exercises matching ``string``.
:param str string: a string, option of one of `testexo` commands.
:rtype: dict
:return: A dictionary with exercises as keys, and sets of integers (seeds)
as values.
"""
splitted = string.split(":")
if len(splitted) == 1:
name = string
seeds = []
elif len(splitted) == 2:
name, seeds = string.split(":")
try:
seeds = [int(seed) for seed in seeds.split(",")]
except ValueError:
raise argparse.ArgumentTypeError("Seeds must be a comma separated list of integers.")
else:
raise argparse.ArgumentTypeError("Seeds must be a comma separated list of integers.")
return (name, seeds)
def argument_parser():
"""Return an argument parser"""
parser = argparse.ArgumentParser()
......@@ -108,7 +85,7 @@ def argument_parser():
)
# Missing
missing = subparsers.add_parser(
missing = subparsers.add_parser( # pylint: disable=unused-variable
'missing',
help='Create missing tests.',
formatter_class=argparse.RawDescriptionHelpFormatter,
......@@ -142,38 +119,6 @@ def argument_parser():
help='Exercises to test. If empty, all exercises are tested.'
)
# Compile
compile_parser = subparsers.add_parser(
'compile',
help='Compile some exercises.',
formatter_class=argparse.RawDescriptionHelpFormatter,
)
compile_parser.add_argument(
"exercise",
metavar='EXERCISE[:SEED[,SEED]]',
nargs='*', type=exercise_argument, default=None,
help='Exercises to compile. If empty, all exercises are compiled.'
)
compile_parser.add_argument(
'-p', '--pipe',
nargs=1,
type=str,
action='append',
help=(
"Commands to run on the LaTeX file before compiling. String '{}' "
"is replaced by the file name; if not, it is appended at the end "
"of the string."
)
)
compile_parser.add_argument(
'-o', '--output',
type=str,
default='exercice.pdf',
help=(
"Output filename. Default is 'exercice.pdf'."
),
)
# Check
check = subparsers.add_parser(
'check',
......@@ -190,17 +135,6 @@ def argument_parser():
help='Exercises to check. If empty, all exercises are checked.'
)
# List exos
lsexos = subparsers.add_parser(
'lsexos',
help=(
"List available exercises. Each line of the output can be used as "
"an argument to other commands."
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
return parser
def do_create(options):
......@@ -216,7 +150,7 @@ def do_create(options):
if ask_confirm("Is the test valid?"):
test.write()
def do_missing(__options):
def do_missing(options): # pylint: disable=unused-argument
"""Action for command line 'missing'."""
tests = TestPerformer()
......@@ -272,28 +206,6 @@ def do_update(options):
if ask_confirm("Is the test valid?"):
test.write()
def do_compile(options):
"""Action for command line 'compile'."""
tests = TestPerformer()
if options.pipe is None:
options.pipe = []
else:
options.pipe = [item[0] for item in options.pipe]
exercise_list = []
for exercise, seeds in options.exercise:
if not seeds:
seeds = [random.randint(0, sys.maxint)]
for seed in seeds:
exercise_list.append(tests.get(exercise, seed).get_exercise())
compile(
exercise_list,
destname=options.output,
pipe=options.pipe,
)
def do_check(options):
"""Run the tests"""
tests = TestPerformer()
......@@ -304,29 +216,23 @@ def do_check(options):
)
)
def do_lsexos(__options):
"""Perform the `lsexos` command."""
tests = TestPerformer()
for exo_id in tests.iter_id():
print(exo_id)
COMMANDS = {
"check": do_check,
"compile": do_compile,
"create": do_create,
"lsexos": do_lsexos,
"missing": do_missing,
"remove": do_remove,
"update": do_update,
}
def main():
def main(argv=None):
"""Main function"""
options = argument_parser().parse_args(sys.argv[1:])
if argv is None:
argv = sys.argv[1:]
options = argument_parser().parse_args(argv)
try:
COMMANDS[options.command](options)
except TestException as error:
except PyromathsException as error:
logging.error(error)
sys.exit(1)
......
......@@ -37,6 +37,7 @@ import unittest
import pyromaths
from pyromaths.outils import System
from pyromaths.cli import PyromathsException
# Logging configuration
logging.basicConfig(level=logging.INFO)
......@@ -50,11 +51,7 @@ def load_tests(*__args, **__kwargs):
for exo in tests.iter_id()
])
class TestException(Exception):
"""Generic exception for this module."""
pass
class ExerciseNotFound(TestException):
class ExerciseNotFound(PyromathsException):
"""Name of exercise cannot be found in known exercises."""
def __init__(self, exercise):
......@@ -74,8 +71,8 @@ def test_path(dirlevel, name, seed, choice):
"%s.%s.%s" % (name, seed, choice)
)
def compile(exercise_list, openpdf=False, destname=None, pipe=None):
"""Compile exercise list as a pdf, and return the resulting pdf name.
def generate(exercise_list, openpdf=False, destname=None, pipe=None):
"""Generate exercise list as a pdf, and return the resulting pdf name.
:param openpdf: Open pdf at the end of compilation.
:param str destname: Destination name (use ``None`` to use the default name).
......@@ -122,17 +119,17 @@ class TestExercise(object):
self.seed = seed
def show(self):
"""Compile exercise, and display its result."""
self.compile(openpdf=1)
"""Generate exercise, and display its result."""
self.generate(openpdf=1)
def get_exercise(self):
"""Return an instanciated exercise."""
random.seed(self.seed)
return self.exercise()
def compile(self, openpdf=0):
"""Compile exercise"""
return compile([self.get_exercise()], openpdf=openpdf)
def generate(self, openpdf=0):
"""Generate exercise"""
return generate([self.get_exercise()], openpdf=openpdf)
def test_path(self, name):
"""Return the path of the file containing expected results."""
......@@ -153,8 +150,8 @@ class TestExercise(object):
def read(self, choice):
"""Read expected test result."""
with codecs.open(self.test_path(choice), "r", "utf8") as file:
return file.read()
with codecs.open(self.test_path(choice), "r", "utf8") as result:
return result.read()
def remove(self):
"""Remove test"""
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment