From e60485405c24bc353757cc3ae12932b9c67eebca Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 19 Oct 2015 00:15:58 +0200 Subject: [PATCH] [test] Add compilation tests --- .../{songbook.py => songbook/__main__.py} | 5 +- setup.py | 2 +- songbook | 7 +- test/dynamic.py | 25 +++ test/test_chordpro/test_parser.py | 39 ++-- test/test_compilation/__init__.py | 0 test/test_compilation/subdir.tex.control | 180 ++++++++++++++++++ test/test_compilation/test_compilation.py | 89 +++++++++ 8 files changed, 318 insertions(+), 29 deletions(-) rename patacrep/{songbook.py => songbook/__main__.py} (97%) create mode 100644 test/dynamic.py create mode 100644 test/test_compilation/__init__.py create mode 100644 test/test_compilation/subdir.tex.control create mode 100644 test/test_compilation/test_compilation.py diff --git a/patacrep/songbook.py b/patacrep/songbook/__main__.py similarity index 97% rename from patacrep/songbook.py rename to patacrep/songbook/__main__.py index 71340cae..8ce858a5 100644 --- a/patacrep/songbook.py +++ b/patacrep/songbook/__main__.py @@ -39,7 +39,10 @@ class VerboseAction(argparse.Action): def argument_parser(args): """Parse arguments""" - parser = argparse.ArgumentParser(description="A song book compiler") + parser = argparse.ArgumentParser( + prog="songbook", + description="A song book compiler", + ) parser.add_argument( '--version', help='Show version', action='version', diff --git a/setup.py b/setup.py index f9977913..4cfaeda0 100755 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ setup( include_package_data=True, entry_points={ 'console_scripts': [ - "songbook = patacrep.songbook:main", + "songbook = patacrep.songbook.__main__:main", ], }, classifiers=[ diff --git a/songbook b/songbook index 80e12f88..f4850774 100755 --- a/songbook +++ b/songbook @@ -1,9 +1,6 @@ -#! /usr/bin/env python3 +#! /bin/sh # Do not edit this file. This file is just a helper file for development test. # It is not part of the distributed software. -"""Command line tool to compile songbooks using the songbook library.""" - -from patacrep.songbook import main -main() +python -m patacrep.songbook $* diff --git a/test/dynamic.py b/test/dynamic.py new file mode 100644 index 00000000..19b721a3 --- /dev/null +++ b/test/dynamic.py @@ -0,0 +1,25 @@ +"""Dynamic creation of test methods.""" + +class DynamicTest(type): + """Metaclass that creates on-the-fly test methods. + + Each class implementing this metaclass must define two class methods: + + - `_iter_testmethods`, which iterate over `(methodname, args)`, where + `methodname` is the name of a test method to create, and `args` is a list + of arguments to pass to `_create_test`; + - `_create_test`, a method returning a test method. + """ + + def __init__(cls, name, bases, nmspc): + super().__init__(name, bases, nmspc) + for methodname, args in cls._iter_testmethods(): + setattr(cls, methodname, cls._create_test(*args)) + + def _iter_testmethods(cls): + """Iterate over test methods.""" + raise NotImplementedError() + + def _create_test(cls, *args, **kwargs): + """Create and return a test method.""" + raise NotImplementedError() diff --git a/test/test_chordpro/test_parser.py b/test/test_chordpro/test_parser.py index 20cafd38..b0760fa8 100644 --- a/test/test_chordpro/test_parser.py +++ b/test/test_chordpro/test_parser.py @@ -10,6 +10,7 @@ from patacrep.build import DEFAULT_CONFIG from patacrep.songs.chordpro import ChordproSong from .. import disable_logging +from .. import dynamic # pylint: disable=unused-import LANGUAGES = { 'tex': 'latex', @@ -17,15 +18,22 @@ LANGUAGES = { 'html': 'html', } -class FileTestMeta(type): - """Metaclass that creates on-the-fly test function according to files. +class FileTest(unittest.TestCase, metaclass=dynamic.DynamicTest): + """Test of chorpro parser, and several renderers. + + For any given `foo.source`, it is parsed as a chordpro file, and should be + rendered as `foo.sgc` with the chordpro renderer, and `foo.tex` with the + latex renderer. - See the :class:`FileTest` documentation for more information. + This class does nothing by itself, but its metaclass populates it with test + methods testing parser and renderers. """ - def __init__(cls, name, bases, nmspc): - super().__init__(name, bases, nmspc) + maxDiff = None + @classmethod + def _iter_testmethods(cls): + """Iterate over song files to test.""" for source in sorted(glob.glob(os.path.join( os.path.dirname(__file__), '*.source', @@ -35,14 +43,13 @@ class FileTestMeta(type): destname = "{}.{}".format(base, dest) if not os.path.exists(destname): continue - setattr( - cls, + yield ( "test_{}_{}".format(os.path.basename(base), dest), - cls._create_test(base, dest), + [base, dest], ) - @staticmethod - def _create_test(base, dest): + @classmethod + def _create_test(cls, base, dest): """Return a function testing that `base` compilation in `dest` format. """ @@ -67,15 +74,3 @@ class FileTestMeta(type): ).format(base=os.path.basename(base), format=dest) return test_parse_render -class FileTest(unittest.TestCase, metaclass=FileTestMeta): - """Test of chorpro parser, and several renderers. - - For any given `foo.source`, it is parsed as a chordpro file, and should be - rendered as `foo.sgc` with the chordpro renderer, and `foo.tex` with the - latex renderer. - - This class does nothing by itself, but its metaclass populates it with test - methods testing parser and renderers. - """ - - maxDiff = None diff --git a/test/test_compilation/__init__.py b/test/test_compilation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/test_compilation/subdir.tex.control b/test/test_compilation/subdir.tex.control new file mode 100644 index 00000000..c026fa96 --- /dev/null +++ b/test/test_compilation/subdir.tex.control @@ -0,0 +1,180 @@ + + + + + + +%% Automatically generated document. +%% You may edit this file but all changes will be overwritten. +%% If you want to change this document, have a look at +%% the templating system. +%% +%% Generated using Songbook + +\makeatletter +\def\input@path{ % + {/home/louis/projets/patacrep/patacrep/test/test_compilation/subdir_datadir/latex/} % + {/home/louis/projets/patacrep/patacrep/test/test_compilation/subdir_datadir2/latex/} % + {/home/louis/projets/patacrep/patacrep/test/test_compilation/latex/} % + {/home/louis/projets/patacrep/patacrep/patacrep/data/latex/} % +} +\makeatother + +\documentclass[ + ]{article} + +\usepackage[ + chorded, +lilypond, +pictures, +guitar, + ]{patacrep} + +\usepackage[utf8]{inputenc} +\usepackage[T1]{fontenc} +\usepackage{lmodern} + + +\PassOptionsToPackage{french}{babel} +\usepackage[english]{babel} +\lang{english} + +\usepackage{graphicx} +\graphicspath{ % + {/home/louis/projets/patacrep/patacrep/test/test_compilation/subdir_datadir/img/} % + {/home/louis/projets/patacrep/patacrep/test/test_compilation/subdir_datadir2/img/} % + {/home/louis/projets/patacrep/patacrep/test/test_compilation/img/} % + {/home/louis/projets/patacrep/patacrep/patacrep/data/img/} % +} + + +\makeatletter +\@ifpackageloaded{hyperref}{}{ + \usepackage{url} + \newcommand{\phantomsection}{} + \newcommand{\hyperlink}[2]{#2} + \newcommand{\href}[2]{\expandafter\url\expandafter{#1}} +} +\makeatother + + +\usepackage{chords} + +\title{Guitar songbook} +\author{The Patacrep Team} + +\newindex{titleidx}{subdir_title} +\newauthorindex{authidx}{subdir_auth} + + +\notenamesout{A}{B}{C}{D}{E}{F}{G} + + +\begin{document} + +\maketitle + + +\showindex{\songindexname}{titleidx} +\showindex{\authorindexname}{authidx} + +% list of chords +\ifchorded + \ifdiagram + \phantomsection + \addcontentsline{toc}{section}{\chordlistname} + \chords + \fi +\fi + +\phantomsection +\addcontentsline{toc}{section}{\songlistname} + +\begin{songs}{titleidx,authidx} +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% songs/./datadir2.sgc + +\selectlanguage{french} + +\beginsong{Image included from a different datadir\\ +Chordpro}[ + by={ + }, + cov={/home/louis/projets/patacrep/patacrep/test/test_compilation/subdir_datadir2/img/datadir2.png}, +] + +\cover + + + +\lilypond{datadir2.ly} +\image{/home/louis/projets/patacrep/patacrep/test/test_compilation/subdir_datadir2/img/datadir2.png} + + +\endsong + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% songs/./datadir2.sg + +\import{subdir_datadir/songs/}{datadir2.sg} + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% songs/./relative.sgc + +\selectlanguage{french} + +\beginsong{Image included from song directory\\ +Chordpro}[ + by={ + }, + cov={/home/louis/projets/patacrep/patacrep/test/test_compilation/subdir_datadir/songs/relative.png}, +] + +\cover + + + +\lilypond{/home/louis/projets/patacrep/patacrep/test/test_compilation/subdir_datadir/songs/relative.ly} +\image{/home/louis/projets/patacrep/patacrep/test/test_compilation/subdir_datadir/songs/relative.png} + + +\endsong + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% songs/./datadir.sgc + +\selectlanguage{french} + +\beginsong{Image included from datadir\\ +Chordpro}[ + by={ + }, + cov={/home/louis/projets/patacrep/patacrep/test/test_compilation/subdir_datadir/img/datadir.png}, +] + +\cover + + + +\lilypond{datadir.ly} +\image{/home/louis/projets/patacrep/patacrep/test/test_compilation/subdir_datadir/img/datadir.png} + + +\endsong + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% songs/./relative.sg + +\import{subdir_datadir/songs/}{relative.sg} + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% songs/./datadir.sg + +\import{subdir_datadir/songs/}{datadir.sg} + +\end{songs} + + + + +\end{document} \ No newline at end of file diff --git a/test/test_compilation/test_compilation.py b/test/test_compilation/test_compilation.py new file mode 100644 index 00000000..374396f1 --- /dev/null +++ b/test/test_compilation/test_compilation.py @@ -0,0 +1,89 @@ +"""Tests for the chordpro parser.""" + +# pylint: disable=too-few-public-methods + +import glob +import os +import subprocess +import unittest + +from .. import dynamic # pylint: disable=unused-import + + +class FileTest(unittest.TestCase, metaclass=dynamic.DynamicTest): + """Test of songbook compilation. + + For any given `foo.sb`, it performs several checks: + - the corresponding tex file is generated; + - the generated tex file matches the `foo.tex.control` control file; + - the compilation (tex, pdf, indexes) works without errors. + + This class does nothing by itself, but its metaclass populates it with test + methods testing parser and renderers. + """ + + maxDiff = None + + @classmethod + def _iter_testmethods(cls): + """Iterate over songbook files to test.""" + for songbook in sorted(glob.glob(os.path.join( + os.path.dirname(__file__), + '*.sb', + ))): + base = songbook[:-len(".sb")] + control = "{}.tex.control".format(base) + if not os.path.exists(control): + continue + yield ( + "test_{}".format(os.path.basename(base)), + [base], + ) + + @classmethod + def _create_test(cls, base): + """Return a function testing that `base` compiles.""" + + def test_compile(self): + """Test that `base` is correctly compiled.""" + if base is None: + return + + songbook = "{}.sb".format(base) + + # Check tex generation + self.assertEqual(0, self.compile_songbook(songbook, "tex")) + + # Check generated tex + control = "{}.tex.control".format(base) + tex = "{}.tex".format(base) + with open(control, 'r', encoding='utf8') as expectfile: + with open(tex, 'r', encoding='utf8') as latexfile: + self.assertMultiLineEqual( + latexfile.read(), + expectfile.read(), + ) + + # Check compilation + self.assertEqual(0, self.compile_songbook(songbook)) + + test_compile.__doc__ = ( + "Test that '{base}' is correctly compiled." + ).format(base=os.path.basename(base)) + return test_compile + + @staticmethod + def compile_songbook(songbook, steps=None): + """Compile songbook, and return the command return code.""" + command = ['python', '-m', 'patacrep.songbook', songbook] + if steps: + command.extend(['--steps', steps]) + + return subprocess.check_call( + command, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + cwd=os.path.dirname(songbook), + ) +