diff options
author | Norbert Preining <norbert@preining.info> | 2019-09-02 13:46:59 +0900 |
---|---|---|
committer | Norbert Preining <norbert@preining.info> | 2019-09-02 13:46:59 +0900 |
commit | e0c6872cf40896c7be36b11dcc744620f10adf1d (patch) | |
tree | 60335e10d2f4354b0674ec22d7b53f0f8abee672 /support/gladtex/tests |
Initial commit
Diffstat (limited to 'support/gladtex/tests')
-rw-r--r-- | support/gladtex/tests/test_cachedconverter.py | 143 | ||||
-rw-r--r-- | support/gladtex/tests/test_caching.py | 149 | ||||
-rw-r--r-- | support/gladtex/tests/test_htmlhandling.py | 404 | ||||
-rw-r--r-- | support/gladtex/tests/test_imagecreation.py | 165 | ||||
-rw-r--r-- | support/gladtex/tests/test_typesetting.py | 218 |
5 files changed, 1079 insertions, 0 deletions
diff --git a/support/gladtex/tests/test_cachedconverter.py b/support/gladtex/tests/test_cachedconverter.py new file mode 100644 index 0000000000..b1c3bb786c --- /dev/null +++ b/support/gladtex/tests/test_cachedconverter.py @@ -0,0 +1,143 @@ +#pylint: disable=too-many-public-methods,import-error,too-few-public-methods,missing-docstring,unused-variable +import os +import shutil +import tempfile +import unittest +from unittest.mock import patch +from gleetex import cachedconverter, image +from gleetex.caching import JsonParserException +from gleetex.image import remove_all + +def get_number_of_files(path): + return len(os.listdir(path)) + +def mk_eqn(eqn, count=0, pos=(1,1)): + """Create formula. Each formula must look like this: + (eqn, pos, path, dsp, count) for self._convert_concurrently, this is a + shorthand with mocking a few values.""" + return (pos, False, eqn) + +def write(path, content='dummy'): + with open(path, 'w', encoding='utf-8') as f: + f.write(str(content)) + +class Tex2imgMock(): + """Could use a proper mock, but this one allows a bit more tricking.""" + def __init__(self, fmt): + self.__format = fmt + self.set_dpi = self.set_transparency = self.set_foreground_color \ + = self.set_background_color = lambda x: None # do nothing + + def create_dvi(self, dvi_fn): + with open(dvi_fn, 'w') as f: + f.write('dummy') + + def create_image(self, dvi_fn): + if os.path.exists(dvi_fn): + os.remove(dvi_fn) + write(os.path.splitext(dvi_fn)[0] + '.' + self.__format.value) + + def convert(self, tx, basename): + write(basename + '.tex', tx) + dvi = basename + '.dvi' + self.create_dvi(dvi) + self.create_image(dvi) + remove_all(dvi, basename + '.tex', basename + '.log', basename + '.aux') + return {'depth': 9, 'height': 8, 'width': 7} + + def parse_log(self, _logdata): + return {} + + +class TestCachedConverter(unittest.TestCase): + #pylint: disable=protected-access + def setUp(self): + self.original_directory = os.getcwd() + self.tmpdir = tempfile.mkdtemp() + os.chdir(self.tmpdir) + + + #pylint: disable=protected-access + def tearDown(self): + # restore static reference to converter + cachedconverter.CachedConverter._converter = image.Tex2img + os.chdir(self.original_directory) + shutil.rmtree(self.tmpdir, ignore_errors=True) + + + + @patch('gleetex.image.Tex2img', Tex2imgMock) + def test_that_subdirectory_is_created(self): + c = cachedconverter.CachedConverter('subdirectory') + formula = ({}, True, '\\textbf{FOO!}') + c.convert_all([formula]) + # one directory exists + self.assertEqual(get_number_of_files('.'), 1, + "Found the following files, expected only 'subdirectory': " + \ + ', '.join(os.listdir('.'))) + # subdirectory contains 1 image and a cache + self.assertEqual(get_number_of_files('subdirectory'), 2, "expected two"+\ + " files, found instead " + repr(os.listdir('subdirectory'))) + + def test_that_unknown_options_trigger_exception(self): + c = cachedconverter.CachedConverter('subdirectory') + self.assertRaises(ValueError, c.set_option, 'cxzbiucxzbiuxzb', 'muh') + + def test_that_invalid_caches_trigger_error_by_default(self): + with open('gladtex.cache', 'w') as f: + f.write('invalid cache') + with self.assertRaises(JsonParserException): + c = cachedconverter.CachedConverter('') + + @patch('gleetex.image.Tex2img', Tex2imgMock) + def test_that_invalid_caches_get_removed_if_specified(self): + formulas = [mk_eqn('tau')] + with open('gladtex.cache', 'w') as f: + f.write('invalid cache') + c = cachedconverter.CachedConverter('.', keep_old_cache=False) + c.convert_all(formulas) + # cache got overridden + with open('gladtex.cache') as f: + self.assertFalse('invalid' in f.read()) + + @patch('gleetex.image.Tex2img', Tex2imgMock) + def test_that_converted_formulas_are_cached(self): + formulas = [mk_eqn('\\tau')] + c = cachedconverter.CachedConverter('.') + c.convert_all(formulas) + self.assertTrue(c.get_data_for('\\tau', False)) + + + @patch('gleetex.image.Tex2img', Tex2imgMock) + def test_that_file_names_are_correctly_picked(self): + formulas = [mk_eqn('\\tau')] + write('eqn000.svg') + write('eqn001.svg') + c = cachedconverter.CachedConverter('') + to_convert = c._get_formulas_to_convert(formulas) + self.assertTrue(len(to_convert), 1) + self.assertEqual(to_convert[0][2], 'eqn002.svg') + + @patch('gleetex.image.Tex2img', Tex2imgMock) + def test_that_all_converted_formulas_are_in_cache_and_meta_info_correct(self): + formulas = [mk_eqn('a_{%d}' % i, pos=(i,i), count=i) for i in range(4)] + c = cachedconverter.CachedConverter('.') + c.convert_all(formulas) + # expect all formulas and a gladtex cache to exist + self.assertEqual(get_number_of_files('.'), len(formulas)+1, + "present files:\n" + ', '.join(os.listdir('.'))) + for pos, dsp, formula in formulas: + data = c.get_data_for(formula, False) + self.assertEqual(data['pos'], {'depth': 9, 'height': 8, 'width': 7}, + "expected the pos as defined in the dummy class") + + @patch('gleetex.image.Tex2img', Tex2imgMock) + def test_that_inline_math_and_display_math_results_in_different_formulas(self): + # two formulas, second is displaymath + formula = r'\sum_{i=0}^n x_i' + formulas = [((1,1), False, formula), ((3,1), True, formula)] + c = cachedconverter.CachedConverter('.') + c.convert_all(formulas) + # expect all formulas and a gladtex cache to exist + self.assertEqual(get_number_of_files('.'), len(formulas)+1, + "present files:\n%s" % ', '.join(os.listdir('.'))) diff --git a/support/gladtex/tests/test_caching.py b/support/gladtex/tests/test_caching.py new file mode 100644 index 0000000000..dd3aebac77 --- /dev/null +++ b/support/gladtex/tests/test_caching.py @@ -0,0 +1,149 @@ +#pylint: disable=too-many-public-methods,import-error,too-few-public-methods,missing-docstring,unused-variable +import os +import shutil +import tempfile +import unittest +from gleetex import caching + +def write(path, content): + with open(path, 'w', encoding='utf-8') as f: + f.write(content) + +class test_caching(unittest.TestCase): + def setUp(self): + self.pos = {'height' : 8, 'depth' : 2, 'width' : 666} + self.original_directory = os.getcwd() + self.tmpdir = tempfile.mkdtemp() + os.chdir(self.tmpdir) + + def tearDown(self): + os.chdir(self.original_directory) + shutil.rmtree(self.tmpdir, ignore_errors=True) + + def test_differently_spaced_formulas_are_the_same(self): + form1 = r'\tau \pi' + form2 = '\tau\\pi' + self.assertTrue(caching.normalize_formula(form1), + caching.normalize_formula(form2)) + + def test_trailing_and_leading_spaces_and_tabs_are_no_problem(self): + u = caching.normalize_formula + form1 = ' hi' + form2 = 'hi ' + form3 = '\thi' + self.assertEqual(u(form1), u(form2)) + self.assertEqual(u(form1), u(form3)) + + def test_that_empty_braces_are_ignored(self): + u = caching.normalize_formula + form1 = r'\sin{}x' + form2 = r'\sin x' + form3 = r'\sin{} x' + self.assertEqual(u(form1), u(form2)) + self.assertEqual(u(form1), u(form3)) + self.assertEqual(u(form2), u(form3)) + + def test_empty_cache_works_fine(self): + write('foo.png', 'muha') + c = caching.ImageCache('file.png') + formula = r"f(x) = \ln(x)" + c.add_formula(formula, self.pos, 'foo.png') + self.assertTrue(c.contains(formula, False)) + + def test_that_invalid_cach_entries_are_detected(self): + # entry is invalid if file doesn't exist + c = caching.ImageCache() + formula = r"f(x) = \ln(x)" + self.assertRaises(OSError, c.add_formula, formula, self.pos, 'file.png') + + def test_that_correct_pos_and_path_are_returned_after_writing_the_cache_back(self): + c = caching.ImageCache() + formula = r"g(x) = \ln(x)" + write('file.png', 'dummy') + c.add_formula(formula, self.pos, 'file.png', displaymath=False) + c.write() + c = caching.ImageCache() + self.assertTrue(c.contains(formula, False)) + data = c.get_data_for(formula, False) + self.assertEqual(data['pos'], self.pos) + self.assertEqual(data['path'], 'file.png') + + + def test_formulas_are_not_added_twice(self): + form1 = r'\ln(x) \neq e^x' + write('spass.png', 'binaryBinary_binary') + c = caching.ImageCache() + for i in range(1,10): + c.add_formula(form1, self.pos, 'spass.png') + self.assertEqual(len(c), 1) + + def test_that_remove_actually_removes(self): + form1 = '\\int e^x dy' + write('happyness.png', 'binaryBinary_binary') + c = caching.ImageCache() + c.add_formula(form1, self.pos, 'happyness.png') + c.remove_formula(form1, False) + self.assertEqual(len(c), 0) + + def test_removal_of_non_existing_formula_raises_exception(self): + c = caching.ImageCache() + self.assertRaises(KeyError, c.remove_formula, 'Haha!', False) + + def test_that_invalid_version_is_detected(self): + c = caching.ImageCache('gladtex.cache') + c._ImageCache__set_version('invalid.stuff') + c.write() + self.assertRaises(caching.JsonParserException, caching.ImageCache, 'gladtex.cache') + + def test_that_invalid_style_is_detected(self): + write('foo.png', "dummy") + c = caching.ImageCache('gladtex.cache') + c.add_formula('\\tau', self.pos, 'foo.png', False) + c.add_formula('\\theta', self.pos, 'foo.png', True) + self.assertRaises(ValueError, c.add_formula, '\\gamma', self.pos, 'foo.png', + 'some stuff') + + def test_that_backslash_in_path_is_replaced_through_slash(self): + c = caching.ImageCache('gladtex.cache') + os.mkdir('bilder') + write(os.path.join('bilder', 'foo.png'), str(0xdeadbeef)) + c.add_formula('\\tau', self.pos, 'bilder\\foo.png', False) + self.assertTrue('/' in c.get_data_for('\\tau', False)['path']) + + def test_that_absolute_paths_trigger_OSError(self): + c = caching.ImageCache('gladtex.cache') + write('foo.png', "dummy") + fn = os.path.abspath('foo.png') + self.assertRaises(OSError, c.add_formula, '\\tau', self.pos, + fn, False) + + def test_that_invalid_caches_are_removed_automatically_if_desired(self): + file_was_removed = lambda x: self.assertFalse(os.path.exists(x), + "expected that file %s was removed, but it still exists" % x) + write('gladtex.cache', 'some non-json rubbish') + c = caching.ImageCache('gladtex.cache', keep_old_cache=False) + file_was_removed('gladtex.cache') + # try the same in a subdirectory + os.mkdir('foo') + cache_path = os.path.join('foo', 'gladtex.cache') + eqn1_path = os.path.join('foo', 'eqn000.png') + eqn2_path = os.path.join('foo', 'eqn003.png') + write(cache_path, 'some non-json rubbish') + write(eqn1_path, 'binary') + write(eqn2_path, 'more binary') + c = caching.ImageCache(cache_path, keep_old_cache=False) + file_was_removed(cache_path) + file_was_removed(eqn1_path) + file_was_removed(eqn2_path) + + def test_that_formulas_in_cache_with_no_file_raise_key_error(self): + c = caching.ImageCache('gladtex.cache', keep_old_cache=False) + write('foo.png', 'dummy') + c.add_formula('\\tau', self.pos, 'foo.png') + c.write() + os.remove('foo.png') + c = caching.ImageCache('gladtex.cache', keep_old_cache=False) + with self.assertRaises(KeyError): + c.get_data_for('foo.png', 'False') + + diff --git a/support/gladtex/tests/test_htmlhandling.py b/support/gladtex/tests/test_htmlhandling.py new file mode 100644 index 0000000000..359ef36965 --- /dev/null +++ b/support/gladtex/tests/test_htmlhandling.py @@ -0,0 +1,404 @@ +#pylint: disable=too-many-public-methods +from functools import reduce +import os, re, shutil, tempfile +import unittest +from gleetex import htmlhandling + + +excl_filename = htmlhandling.HtmlImageFormatter.EXCLUSION_FILE_NAME + +HTML_SKELETON = '''<html><head><meta http-equiv="Content-Type" content="text/html; charset={0}" /> +</head><body>{1}</body>''' + +def read(file_name, mode='r', encoding='utf-8'): + """Read the file, return the string. Close file properly.""" + with open(file_name, mode, encoding=encoding) as handle: + return handle.read() + + +class HtmlparserTest(unittest.TestCase): + def setUp(self): + self.p = htmlhandling.EqnParser() + + def test_start_tags_are_parsed_literally(self): + self.p.feed("<p i='o'>") + self.assertEqual(self.p.get_data()[0], "<p i='o'>", + "The HTML parser should copy start tags literally.") + + def test_that_end_tags_are_copied_literally(self): + self.p.feed("</ p></P>") + self.assertEqual(''.join(self.p.get_data()), "</ p></P>") + + def test_entities_are_unchanged(self): + self.p.feed("
") + self.assertEqual(self.p.get_data()[0], '
') + + def test_charsets_are_copied(self): + self.p.feed('>→') + self.assertEqual(''.join(self.p.get_data()[0]), '>→') + + def test_without_eqn_all_blocks_are_strings(self): + self.p.feed("<html>\n<head/><body><p>42</p><h1>blah</h1></body></html>") + self.assertTrue(reduce(lambda x,y: x and isinstance(y, str), + self.p.get_data()), "all chunks have to be strings") + + def test_equation_is_detected(self): + self.p.feed('<eq>foo \\pi</eq>') + self.assertTrue(isinstance(self.p.get_data()[0], (tuple, list))) + self.assertEqual(self.p.get_data()[0][2], 'foo \\pi') + + def test_tag_followed_by_eqn_is_correctly_recognized(self): + self.p.feed('<p foo="bar"><eq>bar</eq>') + self.assertEqual(self.p.get_data()[0], '<p foo="bar">') + self.assertTrue(isinstance(self.p.get_data(), list), "second item of data must be equation data list") + + def test_document_with_tag_then_eqn_then_tag_works(self): + self.p.feed('<div style="invalid">bar</div><eq>baz</eq><sometag>') + eqn = None + # test should not depend on a specific position of equation, search for + # it + data = self.p.get_data() + for chunk in data: + if isinstance(chunk, (tuple, list)): + eqn = chunk + break + self.assertTrue(isinstance(data[0], str)) + self.assertTrue(eqn is not None, + "No equation found, must be tuple/list object.") + self.assertTrue(isinstance(data[-1], str)) + + def test_equation_is_copied_literally(self): + self.p.feed('<eq ignore="me">my\nlittle\n\\tau</eq>') + self.assertEqual(self.p.get_data()[0][2], 'my\nlittle\n\\tau') + + def test_unclosed_eqns_are_detected(self): + self.assertRaises(htmlhandling.ParseException, self.p.feed, + '<p><eq>\\endless\\formula') + + def test_nested_formulas_trigger_exception(self): + self.assertRaises(htmlhandling.ParseException, self.p.feed, + "<eq>\\pi<eq></eq></eq>") + self.assertRaises(htmlhandling.ParseException, self.p.feed, + "<eq>\\pi<eq></p></eq>") + + def test_formulas_without_displaymath_attribute_are_detected(self): + self.p.feed('<p><eq>\frac12</eq><br /><eq env="inline">bar</eq></p>') + formulas = [c for c in self.p.get_data() if isinstance(c, (tuple, list))] + self.assertEqual(len(formulas), 2) # there should be _2_ formulas + self.assertEqual(formulas[0][1], False) # no displaymath + self.assertEqual(formulas[1][1], False) # no displaymath + + def test_that_unclosed_formulas_detected(self): + self.assertRaises(htmlhandling.ParseException, self.p.feed, + "<eq>\\pi<eq></p>") + self.assertRaises(htmlhandling.ParseException, self.p.feed, + "<eq>\\pi") + + + def test_formula_contains_only_formula(self): + p = htmlhandling.EqnParser() + p.feed("<p><eq>1<i<9</eq></p>") + formula = next(e for e in p.get_data() if isinstance(e, (list, tuple))) + self.assertEqual(formula[-1], "1<i<9") + + p = htmlhandling.EqnParser() + p.feed('<p><eq env="displaymath">test</eq></p>') + formula = next(e for e in p.get_data() if isinstance(e, (list, tuple))) + self.assertEqual(formula[-1], "test") + + p = htmlhandling.EqnParser() + p.feed("<p><eq>1<i<9</eq></p>") + formula = next(e for e in p.get_data() if isinstance(e, (list, tuple))) + self.assertEqual(formula[-1], "1<i<9") + + + def test_formula_with_html_sequences_are_unescaped(self): + self.p.feed('<eq>a>b</eq>') + formula = self.p.get_data()[0] + self.assertEqual(formula[-1], "a>b") + + + def test_displaymath_is_recognized(self): + self.p.feed('<eq env="displaymath">\\sum\limits_{n=1}^{e^i} a^nl^n</eq>') + self.assertEqual(self.p.get_data()[0][1], True) # displaymath flag set + + def test_encoding_is_parsed_from_HTML4(self): + iso8859_1 = HTML_SKELETON.format('iso-8859-15', 'öäüß').encode('iso-8859-1') + self.p.feed(iso8859_1) + self.assertEqual(self.p._EqnParser__encoding, 'iso-8859-15') + + def test_encoding_is_parsed_from_HTML5(self): + document = r"""<!DOCTYPE html> + <html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang=""> + <head><meta charset="utf-8" /> + <meta name="generator" content="pandoc" /> + </head><body><p>hi</p></body></html>""" + self.p.feed(document.encode('utf-8')) + self.assertEqual(self.p._EqnParser__encoding.lower(), 'utf-8') + + + def test_strings_can_be_passed_tO_parser_as_well(self): + # no exception - everything is working as expected + self.p.feed(HTML_SKELETON.format('utf-8', 'æø')) + +class GetPositionTest(unittest.TestCase): + def test_that_line_number_is_correct(self): + self.assertEqual(htmlhandling.get_position('jojo', 0)[0], 0) + self.assertEqual(htmlhandling.get_position('jojo', 3)[0], 0) + self.assertEqual(htmlhandling.get_position('a\njojo', 3)[0], 1) + self.assertEqual(htmlhandling.get_position('a\n\njojo', 3)[0], 2) + + def test_that_position_on_line_is_correct(self): + self.assertEqual(htmlhandling.get_position('jojo', 0)[1], 0) + self.assertEqual(htmlhandling.get_position('jojo', 3)[1], 3) + self.assertEqual(htmlhandling.get_position('a\njojo', 3)[1], 2) + self.assertEqual(htmlhandling.get_position('a\n\njojo', 3)[1], 1) + + + + +class HtmlImageTest(unittest.TestCase): + def setUp(self): + self.pos = {'depth':99, 'height' : 88, 'width' : 77} + self.original_directory = os.getcwd() + self.tmpdir = tempfile.mkdtemp() + os.chdir(self.tmpdir) + + def tearDown(self): + os.chdir(self.original_directory) + shutil.rmtree(self.tmpdir, ignore_errors=True) + + def test_that_no_file_is_written_if_no_content(self): + with htmlhandling.HtmlImageFormatter('foo.html'): + pass + self.assertFalse(os.path.exists('foo.html') ) + + def test_file_if_written_when_content_exists(self): + with htmlhandling.HtmlImageFormatter() as img: + img.format_excluded(self.pos, '\\tau\\tau', 'foo.png') + self.assertTrue(os.path.exists(excl_filename) ) + + def test_written_file_starts_and_ends_more_or_less_properly(self): + with htmlhandling.HtmlImageFormatter('.') as img: + img.format_excluded(self.pos, '\\tau\\tau', 'foo.png') + data = read(htmlhandling.HtmlImageFormatter.EXCLUSION_FILE_NAME, 'r', encoding='utf-8') + self.assertTrue('<html' in data and '</html>' in data) + self.assertTrue('<body' in data and '</body>' in data) + # make sure encoding is specified + self.assertTrue('<meta' in data and 'charset=' in data) + + def test_id_contains_no_special_characters(self): + data = htmlhandling.gen_id('\\tau!\'{}][~^') + for character in {'!', "'", '\\', '{', '}'}: + self.assertFalse(character in data) + + def test_formula_can_consist_only_of_numbers_and_id_is_generated(self): + data = htmlhandling.gen_id('9*8*7=504') + self.assertTrue(data.startswith('form')) + self.assertTrue(data.endswith('504')) + + def test_that_empty_ids_raise_exception(self): + self.assertRaises(ValueError, htmlhandling.gen_id, '') + + def test_that_same_characters_are_not_repeated(self): + id = htmlhandling.gen_id("jo{{{{{{{{ha") + self.assertEqual(id, "jo_ha") + + def test_that_ids_are_max_150_characters_wide(self): + id = htmlhandling.gen_id('\\alpha\\cdot\\gamma + ' * 999) + self.assertTrue(len(id) == 150) + + def test_that_ids_start_with_letter(self): + id = htmlhandling.gen_id('{}\\[]ÖÖÖö9343...·tau') + self.assertTrue(id[0].isalpha()) + + def test_that_link_to_external_image_points_to_file_and_formula(self): + with htmlhandling.HtmlImageFormatter() as img: + formatted_img = img.format_excluded(self.pos, '\\tau\\tau', 'foo.png') + expected_id = htmlhandling.gen_id('\\tau\\tau') + external_file = read(excl_filename, 'r', encoding='utf-8') + # find linked formula path + href = re.search('href="(.*?)"', formatted_img) + self.assertTrue(href != None) + # extract path and id from it + self.assertTrue('#' in href.groups()[0]) + path, id = href.groups()[0].split('#') + self.assertEqual(path, excl_filename) + self.assertEqual(id, expected_id) + + # check external file + self.assertTrue('<p id' in external_file) + self.assertTrue('="'+expected_id in external_file) + + + def test_that_link_to_external_image_points_to_file_basepath_and_formula(self): + os.mkdir('basepath') + with htmlhandling.HtmlImageFormatter('basepath') as img: + formatted_img = img.format_excluded(self.pos, '\\tau\\tau', 'foo.png') + expected_id = htmlhandling.gen_id('\\tau\\tau') + # find linked formula path + href = re.search('href="(.*?)"', formatted_img) + self.assertTrue(href != None) + # extract path and id from it + self.assertTrue('#' in href.groups()[0]) + path, id = href.groups()[0].split('#') + self.assertEqual(path, 'basepath/' + excl_filename) + self.assertEqual(id, expected_id) + + def test_height_and_width_is_in_formatted_html_img_tag(self): + data = None + with htmlhandling.HtmlImageFormatter('foo.html') as img: + data = img.get_html_img(self.pos, '\\tau\\tau', 'foo.png') + self.assertTrue('height=' in data and str(self.pos['height']) in data) + self.assertTrue('width=' in data and str(self.pos['width']) in data) + + def test_no_formula_gets_lost_when_reparsing_external_formula_file(self): + with htmlhandling.HtmlImageFormatter() as img: + img.format_excluded(self.pos, '\\tau' * 999, 'foo.png') + with htmlhandling.HtmlImageFormatter() as img: + img.format_excluded(self.pos, '\\pi' * 666, 'foo_2.png') + data = read(excl_filename) + self.assertTrue('\\tau' in data) + self.assertTrue('\\pi' in data) + + def test_too_long_formulas_are_not_outsourced_if_not_configured(self): + with htmlhandling.HtmlImageFormatter('foo.html') as img: + img.format(self.pos, '\\tau' * 999, 'foo.png') + self.assertFalse(os.path.exists('foo.html')) + + def test_that_too_long_formulas_get_outsourced_if_configured(self): + with htmlhandling.HtmlImageFormatter() as img: + img.set_max_formula_length(90) + img.set_exclude_long_formulas(True) + img.format(self.pos, '\\tau' * 999, 'foo.png') + self.assertTrue(os.path.exists(excl_filename)) + data = read(htmlhandling.HtmlImageFormatter.EXCLUSION_FILE_NAME) + self.assertTrue('\\tau\\tau' in data) + + def test_url_is_included(self): + prefix = "http://crustulus.de/blog" + with htmlhandling.HtmlImageFormatter('foo.html') as img: + img.set_url(prefix) + data = img.format(self.pos, '\epsilon<0', 'foo.png') + self.assertTrue( prefix in data) + + def test_url_doesnt_contain_double_slashes(self): + prefix = "http://crustulus.de/blog/" + with htmlhandling.HtmlImageFormatter('foo.html') as img: + img.set_url(prefix) + data = img.format(self.pos, r'\gamma\text{strahlung}', 'foo.png') + self.assertFalse('//' in data.replace('http://','ignore')) + + # depth is used as negative offset, so negative depth should result in + # positive offset + def test_that_negative_depth_results_in_positive_offset(self): + self.pos['depth'] = '-999' + with htmlhandling.HtmlImageFormatter('foo.html') as img: + data = img.format(self.pos, r'\gamma\text{strahlung}', 'foo.png') + self.assertTrue('align: ' + str(self.pos['depth'])[1:] in data) + + def test_that_displaymath_is_set_or_unset(self): + with htmlhandling.HtmlImageFormatter('foo.html') as img: + data = img.format(self.pos, r'\gamma\text{strahlung}', 'foo.png', + True) + self.assertTrue('="displaymath' in data) + data = img.format(self.pos, r'\gamma\text{strahlung}', 'foo.png', + False) + self.assertTrue('="inlinemath' in data) + + def test_that_alternative_css_class_is_set_correctly(self): + with htmlhandling.HtmlImageFormatter('foo.html') as img: + img.set_display_math_css_class('no1') + img.set_inline_math_css_class('no2') + data = img.format(self.pos, r'\gamma\text{strahlung}', 'foo.png', + True) + self.assertTrue('="no1"' in data) + data = img.format(self.pos, r'\gamma\text{strahlung}', 'foo.png', + False) + self.assertTrue('="no2' in data) + + def test_that_unicode_is_replaced_if_requested(self): + with htmlhandling.HtmlImageFormatter('foo.html') as img: + img.set_replace_nonascii(True) + data = img.format(self.pos, '←', 'foo.png') + self.assertTrue('\\leftarrow' in data, + 'expected: "\\leftarrow" to be in "%s"' % data) + + def test_that_unicode_is_kept_if_not_requested_to_replace(self): + with htmlhandling.HtmlImageFormatter('foo.html') as img: + img.set_replace_nonascii(False) + data = img.format(self.pos, '←', 'foo.png') + self.assertTrue('←' in data) + + def test_formatting_commands_are_stripped(self): + with htmlhandling.HtmlImageFormatter('foo.html') as img: + data = img.format(self.pos, 'a\,b\,c\,d', 'foo.png') + self.assertTrue('a b c d' in data) + data = img.format(self.pos, 'a\,b\;c\ d', 'foo.png') + self.assertTrue('a b c d' in data) + + data = img.format(self.pos, '\Big\{foo\Big\}', 'foo.png') + self.assertTrue('\{foo' in data and '\}' in data) + data = img.format(self.pos, r'\left\{foo\right\}', 'foo.png') + self.assertTrue('\{' in data and 'foo' in data and '\}' in data) + + +def htmleqn(formula, hr=True): + """Format a formula to appear as if it would have been outsourced into an + external file.""" + return '%s\n<p id="%s"><pre>%s</pre></span></p>\n' % (\ + ('<hr/>' if hr else ''), htmlhandling.gen_id(formula), formula) + + +class OutsourcingParserTest(unittest.TestCase): + def setUp(self): + self.html = ('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"' + + '\n "http://www.w3.org/TR/html4/strict.dtd">\n<html>\n<head>\n' + + '<meta http-equiv="content-type" content="text/html; charset=utf-8"/>' + + '<title>Outsourced Formulas</title></head>\n<body>\n<h1>heading</h1>') + + def get_html(self, string): + """Create html string with head / tail und put the specified string into + it.""" + return self.html + string + '\n</body>\n</html>' + + + def test_formulas_are_recognized(self): + data = self.get_html(htmleqn('\\tau')) + parser = htmlhandling.OutsourcedFormulaParser() + parser.feed(data) + self.assertEqual(len(parser.get_formulas()), 1) + + def test_formula_doesnt_contain_surrounding_rubbish(self): + data = self.get_html(htmleqn('\\gamma')) + parser = htmlhandling.OutsourcedFormulaParser() + parser.feed(data) + self.assertEqual(len(parser.get_formulas()), 1) + key = next(iter(parser.get_formulas())) + par = parser.get_formulas()[key] + self.assertFalse('<h1' in par) + self.assertFalse('body>' in par) + self.assertFalse('hr' in par) + + def test_that_header_is_parsed_correctly(self): + p = htmlhandling.OutsourcedFormulaParser() + p.feed(self.get_html(htmleqn('test123', False))) + head = p.get_head() + self.assertTrue('DOCTYPE' in head) + self.assertTrue('<html' in head) + self.assertTrue('<title' in head) + self.assertTrue('</title' in head) + self.assertTrue('</head' in head) + self.assertTrue('<meta' in head) + self.assertTrue('charset=' in head) + + def test_multiple_formulas_are_recognized_correctly(self): + p = htmlhandling.OutsourcedFormulaParser() + p.feed(self.get_html(htmleqn('\\tau', False) + '\n' + + htmleqn('\\gamma') + '\n' + htmleqn('\\epsilon<0'))) + forms = p.get_formulas() + self.assertEqual(len(forms), 3) + self.assertTrue('\\gamma' in forms.values()) + self.assertTrue('\\gamma' in forms.values()) + self.assertTrue('\\epsilon<0' in forms.values()) + diff --git a/support/gladtex/tests/test_imagecreation.py b/support/gladtex/tests/test_imagecreation.py new file mode 100644 index 0000000000..83f87b22d2 --- /dev/null +++ b/support/gladtex/tests/test_imagecreation.py @@ -0,0 +1,165 @@ +#pylint: disable=too-many-public-methods,import-error,too-few-public-methods,missing-docstring,unused-variable +import os +import pprint +import shutil +import tempfile +import unittest +from unittest.mock import patch +from subprocess import SubprocessError + +import gleetex.image as image +from gleetex.image import Format +from gleetex.typesetting import LaTeXDocument as doc + +LATEX_ERROR_OUTPUT = r""" +This is pdfTeX, Version 3.14159265-2.6-1.40.17 (TeX Live 2016/Debian) (preloaded format=latex) + restricted \write18 enabled. +entering extended mode +(./bla.tex +LaTeX2e <2016/03/31> patch level 3 +Babel <3.9r> and hyphenation patterns for 10 language(s) loaded. +(/usr/share/texlive/texmf-dist/tex/latex/base/article.cls +Document Class: article 2014/09/29 v1.4h Standard LaTeX document class +(/usr/share/texlive/texmf-dist/tex/latex/base/size10.clo)) (./bla.aux) +! Undefined control sequence. +<recently read> \foo + +l.3 $\foo + $ +No pages of output. +Transcript written on bla.log. +""" + + +def call_dummy(_lklklklklk, **blah): + """Dummy to prohibit subprocess execution.""" + return str(blah) + + + + + +#pylint: disable=unused-argument +def latex_error_mock(_cmd, **quark): + """Mock an error case.""" + raise SubprocessError(LATEX_ERROR_OUTPUT) + +#pylint: disable=unused-argument +def dvipng_mock(cmd, **kwargs): + """Mock an error case.""" + fn = None + try: + fn = next(e for e in cmd if e.endswith('.png')) + except StopIteration: + try: + fn = next(e for e in cmd if e.endswith('.dvi')) + except StopIteration: + pass + if fn: + with open(fn, 'w') as f: + f.write("test case") + return 'This is dvipng 1.14 Copyright 2002-2010 Jan-Ake Larsson\n ' + \ + 'depth=3 height=9 width=22' + +def touch(files): + for file in files: + dirname = os.path.dirname(file) + if dirname and not os.path.exists(dirname): + os.makedirs(dirname) + with open(file, 'w') as f: + f.write('\n') + + +class test_imagecreation(unittest.TestCase): + def setUp(self): + self.original_directory = os.getcwd() + self.tmpdir = tempfile.mkdtemp() + os.chdir(self.tmpdir) + image.Tex2img.call = call_dummy + + def tearDown(self): + os.chdir(self.original_directory) + shutil.rmtree(self.tmpdir, ignore_errors=True) + + @patch('gleetex.image.proc_call', latex_error_mock) + def test_that_error_of_incorrect_formula_is_parsed_correctly(self): + i = image.Tex2img(Format.Png) + try: + i.create_dvi(doc("\\foo"), 'foo.png') + except SubprocessError as e: + # expect undefined control sequence in error output + self.assertTrue('Undefined' in e.args[0]) + + @patch('gleetex.image.proc_call', call_dummy) + def test_that_intermediate_files_are_removed_after_successful_run(self): + files = ['foo.log', 'foo.aux', 'foo.tex'] + touch(files) + i = image.Tex2img(Format.Png) + i.create_dvi(doc("\\frac\\pi\\tau"), 'foo.png') + for intermediate_file in files: + self.assertFalse(os.path.exists(intermediate_file), "File " + + intermediate_file + " should not exist.") + + @patch('gleetex.image.proc_call', latex_error_mock) + def test_that_intermediate_files_are_removed_when_exception_is_raised(self): + files = ['foo.log', 'foo.aux', 'foo.tex'] + touch(files) + # error case + i = image.Tex2img(Format.Png) + try: + i.convert(doc("\\foo"), 'foo') + except SubprocessError as e: + for intermediate_file in files: + self.assertFalse(os.path.exists(intermediate_file), "File " + + intermediate_file + " should not exist.") + + @patch('gleetex.image.proc_call', dvipng_mock) + def test_intermediate_files_are_removed(self): + files = ['foo.tex', 'foo.log', 'foo.aux', 'foo.dvi'] + touch(files) + i = image.Tex2img(Format.Png) + i.convert(doc('\\hat{x}'), 'foo') + for intermediate_file in files: + self.assertFalse(os.path.exists(intermediate_file)) + + @patch('gleetex.image.proc_call', latex_error_mock) + def test_intermediate_files_are_removed_when_exception_raised(self): + files = ['foo.tex', 'foo.log', 'foo.aux', 'foo.dvi'] + touch(files) + i = image.Tex2img(Format.Png) + try: + i.convert(doc('\\hat{x}'), 'foo') + except SubprocessError: + self.assertFalse(os.path.exists('foo.tex')) + self.assertFalse(os.path.exists('foo.dvi')) + self.assertFalse(os.path.exists('foo.log')) + self.assertFalse(os.path.exists('foo.aux')) + + + @patch('gleetex.image.proc_call', lambda *x, **y: 'This is dvipng 1.14 ' + \ + 'Copyright 2002-2010 Jan-Ake Larsson\n depth=3 height=9 width=22') + def test_that_values_for_positioning_png_are_returned(self): + i = image.Tex2img(Format.Png) + posdata = i.create_image('foo.dvi') + self.assertTrue('height' in posdata) + self.assertTrue('width' in posdata) + + + @patch('gleetex.image.proc_call', dvipng_mock) + def test_that_output_file_names_with_paths_are_ok_and_log_is_removed(self): + fname = lambda f: os.path.join('bilder', 'farce.' + f) + touch([fname('log'), fname('png')]) + t = image.Tex2img(Format.Png) + t.convert(doc(r"\hat{es}\pi\pi\ldots"), fname('')[:-1]) + self.assertFalse(os.path.exists("farce.log")) + self.assertTrue(os.path.exists(fname('png')), + "couldn't find file {}, directory structure:\n{}".format( + fname('png'), ''.join(pprint.pformat(list(os.walk('.')))))) + self.assertFalse(os.path.exists(fname('log'))) + + +class TestImageResolutionCorrectlyCalculated(unittest.TestCase): + def test_sizes_are_correctly_calculated(self): + self.assertEqual(int(image.fontsize2dpi(12)), 115) + self.assertEqual(int(image.fontsize2dpi(10)), 96) + diff --git a/support/gladtex/tests/test_typesetting.py b/support/gladtex/tests/test_typesetting.py new file mode 100644 index 0000000000..374fe38511 --- /dev/null +++ b/support/gladtex/tests/test_typesetting.py @@ -0,0 +1,218 @@ +#pylint: disable=too-many-public-methods,import-error,too-few-public-methods,missing-docstring,unused-variable +import unittest +from gleetex.typesetting import LaTeXDocument +import gleetex.typesetting as typesetting + +class test_typesetting(unittest.TestCase): + def test_formula_is_embedded(self): + formula = 'E = m \\cdot c^2' + doc = LaTeXDocument(formula) + self.assertTrue(formula in str(doc), + "formula must be contained in LaTeX typesetting as it was inserted.") + + def test_if_displaymath_unset_correct_env_used(self): + doc = LaTeXDocument(r'A = \pi r^2') + doc.set_displaymath(False) + self.assertTrue('\\(' in str(doc)) + self.assertTrue('\\)' in str(doc)) + + def test_if_displaymath_is_set_correct_env_used(self): + doc = LaTeXDocument(r'A = \pi r^2') + doc.set_displaymath(True) + self.assertTrue('\\[' in str(doc)) + self.assertTrue('\\]' in str(doc)) + + def test_preamble_is_included(self): + preamble = '\\usepackage{eurosym}' + doc = LaTeXDocument('moooo') + doc.set_preamble_string(preamble) + self.assertTrue(preamble in str(doc)) + + def test_obviously_wrong_encoding_trigger_exception(self): + doc = LaTeXDocument('f00') + self.assertRaises(ValueError, doc.set_encoding, 'latin1:') + self.assertRaises(ValueError, doc.set_encoding, 'utf66') + # the following passes (assertRaisesNot) + doc.set_encoding('utf-8') + + def test_that_latex_maths_env_is_used(self): + doc = LaTeXDocument('f00') + doc.set_latex_environment('flalign*') + self.assertTrue(r'\begin{flalign*}' in str(doc)) + self.assertTrue(r'\end{flalign*}' in str(doc)) + +################################################################################ + + +class test_replace_unicode_characters(unittest.TestCase): + def test_that_ascii_strings_are_returned_verbatim(self): + for string in ['abc.\\', '`~[]}{:<>']: + textmode = typesetting.replace_unicode_characters(string, False) + self.assertEqual(textmode, string, 'expected %s, got %s' % (string, textmode)) + mathmode = typesetting.replace_unicode_characters(string, True) + self.assertEqual(textmode, string, 'expected %s, got %s' % (string, mathmode)) + + def test_that_alphabetical_characters_are_replaced_by_default(self): + textmode = typesetting.replace_unicode_characters('ö', False) + self.assertTrue('\\"' in textmode) + mathmode = typesetting.replace_unicode_characters('ö', True) + self.assertTrue('\\ddot' in mathmode) + + def test_that_alphabetical_characters_are_kept_in_text_mode_if_specified(self): + self.assertEqual(typesetting.replace_unicode_characters('ö', False, # text mode + replace_alphabeticals=False), 'ö') + self.assertEqual(typesetting.replace_unicode_characters('æ', False, + replace_alphabeticals=False), 'æ') + + def test_that_alphanumericals_are_replaced_in_mathmode_even_if_replace_alphabeticals_set(self): + self.assertNotEqual(typesetting.replace_unicode_characters('öäü', True, + replace_alphabeticals=True), 'öäü') + self.assertNotEqual(typesetting.replace_unicode_characters('æø', True, + replace_alphabeticals=True), 'æø') + + + def test_that_charachters_not_present_in_file_raise_exception(self): + with self.assertRaises(ValueError): + typesetting.replace_unicode_characters('€', True) + + def test_that_formulas_are_replaced(self): + self.assertNotEqual(typesetting.replace_unicode_characters('π', True), + 'π') + self.assertNotEqual(typesetting.replace_unicode_characters('π', False), + 'π') + +class test_get_matching_brace(unittest.TestCase): + def test_closing_brace_found_when_only_one_brace_present(self): + text = 'text{ok}' + self.assertEqual(typesetting.get_matching_brace(text, 4), len(text) - 1) + self.assertEqual(typesetting.get_matching_brace(text + 'foo', 4), len(text) - 1) + + def test_outer_brace_found(self): + text = 'text{o, bla\\"{o}dfdx.}ds' + self.assertEqual(typesetting.get_matching_brace(text, 4), len(text)-3) + + def test_inner_brace_is_matched(self): + text = 'text{o, bla\\"{o}dfdx.}ds' + self.assertEqual(typesetting.get_matching_brace(text, 13), 15) + + def test_that_unmatched_braces_raise_exception(self): + with self.assertRaises(ValueError): + typesetting.get_matching_brace('text{foooooooo', 4) + with self.assertRaises(ValueError): + typesetting.get_matching_brace('text{jo\"{o....}', 4) + + def test_wrong_position_for_opening_brace_raises(self): + with self.assertRaises(ValueError): + typesetting.get_matching_brace('moo', 1) + + +class test_escape_unicode_maths(unittest.TestCase): + """These tests assume that the tests written above work!""" + def test_that_mathmode_and_textmode_are_treated_differently(self): + math = typesetting.escape_unicode_maths('ö') + self.assertNotEqual(math, 'ö') + text = typesetting.escape_unicode_maths('\\text{ö}') + self.assertFalse('ö' in text) + # check whether characters got transcribed differently; it's enough to + # check one character of the generated sequence, they should differ + self.assertNotEqual(math[:2], text[6:8]) + + def test_that_flag_to_preserve_alphas_is_passed_through(self): + res = typesetting.escape_unicode_maths('\\text{ö}', + replace_alphabeticals=False) + self.assertEqual(res, '\\text{ö}') + + def test_that_all_characters_are_preserved_when_no_replacements_happen(self): + text = 'This is a \\text{test} mate.' + self.assertEqual(typesetting.escape_unicode_maths(text), text) + self.assertEqual(typesetting.escape_unicode_maths(text, + replace_alphabeticals=False), text) + text = 'But yeah but no' * 20 + ', oh my god!' + self.assertEqual(typesetting.escape_unicode_maths(text), text) + self.assertEqual(typesetting.escape_unicode_maths(text, + replace_alphabeticals=False), text) + + def test_that_everything_around_surrounded_character_is_preserved(self): + text = 'This is a \\text{über} test. ;)' + result = typesetting.escape_unicode_maths(text, + replace_alphabeticals=True) + ue_pos = text.index('ü') + # text in front is unchanged + self.assertEqual(result[:ue_pos], text[:ue_pos]) + # find b character, which is the start of the remaining string + b_pos = result[ue_pos:].find('b') + ue_pos + # check that text after umlaut matches + self.assertEqual(result[b_pos:], text[ue_pos+1:]) + + text = 'But yeah but no' * 20 + ', oh my god!ø' + o_strok_pos = text.index('ø') + res = typesetting.escape_unicode_maths(text) + self.assertEqual(res[:o_strok_pos], text[:o_strok_pos]) + + def test_that_unknown_unicode_characters_raise_exception(self): + # you know that Santa Clause character? Seriously, if you don't know it, + # you should have a look. LaTeX does indeed not have command for this + # (2016, one never knows) + santa = chr(127877) + with self.assertRaises(typesetting.DocumentSerializationException): + typesetting.escape_unicode_maths(santa) + + def test_that_two_text_environments_preserve_all_characters(self): + text = r'a\cdot b \text{equals} b\cdot c} \mbox{ is not equal } u^{v\cdot k}' + self.assertEqual(typesetting.escape_unicode_maths(text), text) + + def test_color_names_in_backgroundare_accepted(self): + doc = LaTeXDocument(r'A = \pi r^2') + doc.set_background_color('cyan') + doc = str(doc) + self.assertTrue('pagecolor{cyan}' in doc, + "Expected \\pagecolor in document, got: %s" % doc) + self.assertTrue('\\color' not in doc) + self.assertFalse('definecolor' in doc) + + def test_color_names_in_foregroundare_accepted(self): + doc = LaTeXDocument(r'A = \pi r^2') + doc.set_foreground_color('cyan') + doc = str(doc) + self.assertTrue('pagecolor' not in doc, + "Expected \\pagecolor in document, got: %s" % doc) + self.assertTrue('\\color{cyan' in doc, + "expeccted \\color{cyan, got:\n" + doc) + self.assertFalse('definecolor' in doc) + + def test_hex_colours_with_leading_0s_work(self): + doc = LaTeXDocument(r'A = \pi r^2') + doc.set_background_color('00FFCC') + doc = str(doc) + self.assertTrue('pagecolor{background}' in doc, + "Expected \\pagecolor in document, got: %s" % doc) + self.assertTrue('definecolor' in doc) + self.assertTrue('00FFCC' in doc) + + def test_color_rgb_in_foregroundare_accepted(self): + doc = LaTeXDocument(r'A = \pi r^2') + doc.set_foreground_color('FFAACC') + doc = str(doc) + self.assertTrue('pagecolor{}' not in doc, + "Expected \\pagecolor in document, got: %s" % doc) + self.assertTrue('\\color{foreground' in doc, + "document misses \\color command: %s" % doc) + self.assertTrue('definecolor' in doc) + self.assertTrue('FFAACC' in doc) + + def test_color_rgb_in_backgroundare_accepted(self): + doc = LaTeXDocument(r'A = \pi r^2') + doc.set_background_color('FFAACC') + doc = str(doc) + self.assertTrue('pagecolor{background}' in doc, + "Expected \\pagecolor in document, got: %s" % doc) + self.assertTrue('\\color' not in doc) + self.assertTrue('definecolor' in doc) + self.assertTrue('FFAACC' in doc) + + def test_no_colors_no_color_definitions(self): + doc = str(LaTeXDocument(r'A = \pi r^2')) + self.assertFalse('pagecolor' in doc) + self.assertFalse('\\color' in doc) + self.assertFalse('definecolor' in doc) + |