diff --git a/tests/i18n/test_compilation.py b/tests/i18n/test_compilation.py index 57de5d9a7ea..ac2d468f3e9 100644 --- a/tests/i18n/test_compilation.py +++ b/tests/i18n/test_compilation.py @@ -3,9 +3,7 @@ from __future__ import unicode_literals import gettext as gettext_module import os -import shutil import stat -import tempfile import unittest from subprocess import Popen @@ -18,43 +16,20 @@ from django.core.management.utils import find_command from django.test import SimpleTestCase, mock, override_settings from django.test.utils import captured_stderr, captured_stdout from django.utils import six, translation -from django.utils._os import upath from django.utils.encoding import force_text from django.utils.six import StringIO from django.utils.translation import ugettext +from .utils import RunInTmpDirMixin, copytree + has_msgfmt = find_command('msgfmt') -source_code_dir = os.path.dirname(upath(__file__)) @unittest.skipUnless(has_msgfmt, 'msgfmt is mandatory for compilation tests') -class MessageCompilationTests(SimpleTestCase): +class MessageCompilationTests(RunInTmpDirMixin, SimpleTestCase): work_subdir = 'commands' - def setUp(self): - self._cwd = os.getcwd() - self.work_dir = tempfile.mkdtemp(prefix='i18n_') - self.test_dir = os.path.abspath(os.path.join(self.work_dir, self.work_subdir)) - shutil.copytree(os.path.join(source_code_dir, self.work_subdir), self.test_dir) - # Make sure we step out of the temporary working tree before we - # remove it as we might be pulling the rug from under our own feet - # othewise. Rhis is especially true on Windows. - # Remember cleanup actions registered with addCleanup() are called in - # reverse so this ordering is important: - self.addCleanup(self._rmrf, self.test_dir) - self.addCleanup(os.chdir, self._cwd) - os.chdir(self.test_dir) - - def _rmrf(self, dname): - if os.path.commonprefix([self.test_dir, os.path.abspath(dname)]) != self.test_dir: - return - shutil.rmtree(dname) - - def rmfile(self, filepath): - if os.path.exists(filepath): - os.remove(filepath) - class PoFileTests(MessageCompilationTests): @@ -87,10 +62,6 @@ class PoFileContentsTests(MessageCompilationTests): LOCALE = 'fr' MO_FILE = 'locale/%s/LC_MESSAGES/django.mo' % LOCALE - def setUp(self): - super(PoFileContentsTests, self).setUp() - self.addCleanup(os.unlink, os.path.join(self.test_dir, self.MO_FILE)) - def test_percent_symbol_in_po_file(self): call_command('compilemessages', locale=[self.LOCALE], stdout=StringIO()) self.assertTrue(os.path.exists(self.MO_FILE)) @@ -106,8 +77,6 @@ class MultipleLocaleCompilationTests(MessageCompilationTests): localedir = os.path.join(self.test_dir, 'locale') self.MO_FILE_HR = os.path.join(localedir, 'hr/LC_MESSAGES/django.mo') self.MO_FILE_FR = os.path.join(localedir, 'fr/LC_MESSAGES/django.mo') - self.addCleanup(self.rmfile, os.path.join(localedir, self.MO_FILE_HR)) - self.addCleanup(self.rmfile, os.path.join(localedir, self.MO_FILE_FR)) def test_one_locale(self): with override_settings(LOCALE_PATHS=[os.path.join(self.test_dir, 'locale')]): @@ -131,8 +100,7 @@ class ExcludedLocaleCompilationTests(MessageCompilationTests): def setUp(self): super(ExcludedLocaleCompilationTests, self).setUp() - shutil.copytree('canned_locale', 'locale') - self.addCleanup(self._rmrf, os.path.join(self.test_dir, 'locale')) + copytree('canned_locale', 'locale') def test_command_help(self): with captured_stdout(), captured_stderr(): @@ -170,15 +138,11 @@ class ExcludedLocaleCompilationTests(MessageCompilationTests): class CompilationErrorHandling(MessageCompilationTests): def test_error_reported_by_msgfmt(self): # po file contains wrong po formatting. - mo_file = 'locale/ja/LC_MESSAGES/django.mo' - self.addCleanup(self.rmfile, os.path.join(self.test_dir, mo_file)) with self.assertRaises(CommandError): call_command('compilemessages', locale=['ja'], verbosity=0) def test_msgfmt_error_including_non_ascii(self): # po file contains invalid msgstr content (triggers non-ascii error content). - mo_file = 'locale/ko/LC_MESSAGES/django.mo' - self.addCleanup(self.rmfile, os.path.join(self.test_dir, mo_file)) # Make sure the output of msgfmt is unaffected by the current locale. env = os.environ.copy() env.update({str('LANG'): str('C')}) @@ -202,11 +166,6 @@ class ProjectAndAppTests(MessageCompilationTests): PROJECT_MO_FILE = 'locale/%s/LC_MESSAGES/django.mo' % LOCALE APP_MO_FILE = 'app_with_locale/locale/%s/LC_MESSAGES/django.mo' % LOCALE - def setUp(self): - super(ProjectAndAppTests, self).setUp() - self.addCleanup(self.rmfile, os.path.join(self.test_dir, self.PROJECT_MO_FILE)) - self.addCleanup(self.rmfile, os.path.join(self.test_dir, self.APP_MO_FILE)) - class FuzzyTranslationTest(ProjectAndAppTests): diff --git a/tests/i18n/test_extraction.py b/tests/i18n/test_extraction.py index 8d8bedabe5c..811edc15f51 100644 --- a/tests/i18n/test_extraction.py +++ b/tests/i18n/test_extraction.py @@ -5,12 +5,10 @@ import io import os import re import shutil -import tempfile import time import warnings from unittest import SkipTest, skipUnless -from django.conf import settings from django.core import management from django.core.management import execute_from_command_line from django.core.management.base import CommandError @@ -20,62 +18,23 @@ from django.core.management.utils import find_command from django.test import SimpleTestCase, mock, override_settings from django.test.utils import captured_stderr, captured_stdout from django.utils import six -from django.utils._os import upath from django.utils.encoding import force_text from django.utils.six import StringIO from django.utils.translation import TranslatorCommentWarning +from .utils import POFileAssertionMixin, RunInTmpDirMixin, copytree + LOCALE = 'de' has_xgettext = find_command('xgettext') -source_code_dir = os.path.dirname(upath(__file__)) - - -class POFileAssertionMixin(object): - - def _assertPoKeyword(self, keyword, expected_value, haystack, use_quotes=True): - q = '"' - if use_quotes: - expected_value = '"%s"' % expected_value - q = "'" - needle = '%s %s' % (keyword, expected_value) - expected_value = re.escape(expected_value) - return self.assertTrue(re.search('^%s %s' % (keyword, expected_value), haystack, re.MULTILINE), - 'Could not find %(q)s%(n)s%(q)s in generated PO file' % {'n': needle, 'q': q}) - - def assertMsgId(self, msgid, haystack, use_quotes=True): - return self._assertPoKeyword('msgid', msgid, haystack, use_quotes=use_quotes) @skipUnless(has_xgettext, 'xgettext is mandatory for extraction tests') -class ExtractorTests(POFileAssertionMixin, SimpleTestCase): +class ExtractorTests(POFileAssertionMixin, RunInTmpDirMixin, SimpleTestCase): work_subdir = 'commands' PO_FILE = 'locale/%s/LC_MESSAGES/django.po' % LOCALE - def setUp(self): - self.work_dir = tempfile.mkdtemp(prefix='i18n_') - self.test_dir = os.path.abspath(os.path.join(self.work_dir, self.work_subdir)) - shutil.copytree(os.path.join(source_code_dir, self.work_subdir), self.test_dir) - self._cwd = os.getcwd() - - def _rmrf(self, dname): - if os.path.commonprefix([self.test_dir, os.path.abspath(dname)]) != self.test_dir: - return - shutil.rmtree(dname) - - def rmfile(self, filepath): - if os.path.exists(filepath): - os.remove(filepath) - - def tearDown(self): - os.chdir(self.test_dir) - try: - self._rmrf('locale/%s' % LOCALE) - except OSError: - pass - os.chdir(self._cwd) - def _run_makemessages(self, **options): os.chdir(self.test_dir) out = StringIO() @@ -177,7 +136,6 @@ class ExtractorTests(POFileAssertionMixin, SimpleTestCase): class BasicExtractorTests(ExtractorTests): def test_comments_extractor(self): - os.chdir(self.test_dir) management.call_command('makemessages', locale=[LOCALE], verbosity=0) self.assertTrue(os.path.exists(self.PO_FILE)) with io.open(self.PO_FILE, 'r', encoding='utf-8') as fp: @@ -211,7 +169,6 @@ class BasicExtractorTests(ExtractorTests): ) def test_special_char_extracted(self): - os.chdir(self.test_dir) management.call_command('makemessages', locale=[LOCALE], verbosity=0) self.assertTrue(os.path.exists(self.PO_FILE)) with io.open(self.PO_FILE, 'r', encoding='utf-8') as fp: @@ -219,7 +176,6 @@ class BasicExtractorTests(ExtractorTests): self.assertMsgId("Non-breaking space\u00a0:", po_contents) def test_blocktrans_trimmed(self): - os.chdir(self.test_dir) management.call_command('makemessages', locale=[LOCALE], verbosity=0) self.assertTrue(os.path.exists(self.PO_FILE)) with open(self.PO_FILE, 'r') as fp: @@ -237,7 +193,6 @@ class BasicExtractorTests(ExtractorTests): self.assertTrue(MakeMessagesCommand.leave_locale_alone) def test_extraction_error(self): - os.chdir(self.test_dir) msg = ( 'Translation blocks must not include other block tags: blocktrans ' '(file %s, line 3)' % os.path.join('templates', 'template_with_error.tpl') @@ -248,9 +203,7 @@ class BasicExtractorTests(ExtractorTests): self.assertFalse(os.path.exists('./templates/template_with_error.tpl.py')) def test_unicode_decode_error(self): - os.chdir(self.test_dir) shutil.copyfile('./not_utf8.sample', './not_utf8.txt') - self.addCleanup(self.rmfile, os.path.join(self.test_dir, 'not_utf8.txt')) out = StringIO() management.call_command('makemessages', locale=[LOCALE], stdout=out) self.assertIn("UnicodeDecodeError: skipped file not_utf8.txt in .", @@ -258,9 +211,7 @@ class BasicExtractorTests(ExtractorTests): def test_extraction_warning(self): """test xgettext warning about multiple bare interpolation placeholders""" - os.chdir(self.test_dir) shutil.copyfile('./code.sample', './code_sample.py') - self.addCleanup(self.rmfile, os.path.join(self.test_dir, 'code_sample.py')) out = StringIO() management.call_command('makemessages', locale=[LOCALE], stdout=out) self.assertIn("code_sample.py:4", force_text(out.getvalue())) @@ -271,7 +222,6 @@ class BasicExtractorTests(ExtractorTests): {% trans %} and {% blocktrans %} template tags. Refs #14806. """ - os.chdir(self.test_dir) management.call_command('makemessages', locale=[LOCALE], verbosity=0) self.assertTrue(os.path.exists(self.PO_FILE)) with open(self.PO_FILE, 'r') as fp: @@ -302,7 +252,6 @@ class BasicExtractorTests(ExtractorTests): self.assertMsgId("Translatable literal #8d %(a)s", po_contents) def test_context_in_single_quotes(self): - os.chdir(self.test_dir) management.call_command('makemessages', locale=[LOCALE], verbosity=0) self.assertTrue(os.path.exists(self.PO_FILE)) with open(self.PO_FILE, 'r') as fp: @@ -317,7 +266,6 @@ class BasicExtractorTests(ExtractorTests): def test_template_comments(self): """Template comment tags on the same line of other constructs (#19552)""" - os.chdir(self.test_dir) # Test detection/end user reporting of old, incorrect templates # translator comments syntax with warnings.catch_warnings(record=True) as ws: @@ -432,9 +380,7 @@ class BasicExtractorTests(ExtractorTests): def test_po_file_encoding_when_updating(self): """Update of PO file doesn't corrupt it with non-UTF-8 encoding on Python3+Windows (#23271)""" BR_PO_BASE = 'locale/pt_BR/LC_MESSAGES/django' - os.chdir(self.test_dir) shutil.copyfile(BR_PO_BASE + '.pristine', BR_PO_BASE + '.po') - self.addCleanup(self.rmfile, os.path.join(self.test_dir, 'locale', 'pt_BR', 'LC_MESSAGES', 'django.po')) management.call_command('makemessages', locale=['pt_BR'], verbosity=0) self.assertTrue(os.path.exists(BR_PO_BASE + '.po')) with io.open(BR_PO_BASE + '.po', 'r', encoding='utf-8') as fp: @@ -447,7 +393,6 @@ class JavascriptExtractorTests(ExtractorTests): PO_FILE = 'locale/%s/LC_MESSAGES/djangojs.po' % LOCALE def test_javascript_literals(self): - os.chdir(self.test_dir) _, po_contents = self._run_makemessages(domain='djangojs') self.assertMsgId('This literal should be included.', po_contents) self.assertMsgId('gettext_noop should, too.', po_contents) @@ -522,15 +467,6 @@ class SymlinkExtractorTests(ExtractorTests): super(SymlinkExtractorTests, self).setUp() self.symlinked_dir = os.path.join(self.test_dir, 'templates_symlinked') - def tearDown(self): - super(SymlinkExtractorTests, self).tearDown() - os.chdir(self.test_dir) - try: - os.remove(self.symlinked_dir) - except OSError: - pass - os.chdir(self._cwd) - def test_symlink(self): # On Python < 3.2 os.symlink() exists only on Unix if hasattr(os, 'symlink'): @@ -561,17 +497,7 @@ class CopyPluralFormsExtractorTests(ExtractorTests): PO_FILE_ES = 'locale/es/LC_MESSAGES/django.po' - def tearDown(self): - super(CopyPluralFormsExtractorTests, self).tearDown() - os.chdir(self.test_dir) - try: - self._rmrf('locale/es') - except OSError: - pass - os.chdir(self._cwd) - def test_copy_plural_forms(self): - os.chdir(self.test_dir) management.call_command('makemessages', locale=[LOCALE], verbosity=0) self.assertTrue(os.path.exists(self.PO_FILE)) with open(self.PO_FILE, 'r') as fp: @@ -580,7 +506,6 @@ class CopyPluralFormsExtractorTests(ExtractorTests): def test_override_plural_forms(self): """Ticket #20311.""" - os.chdir(self.test_dir) management.call_command('makemessages', locale=['es'], extensions=['djtpl'], verbosity=0) self.assertTrue(os.path.exists(self.PO_FILE_ES)) with io.open(self.PO_FILE_ES, 'r', encoding='utf-8') as fp: @@ -594,7 +519,6 @@ class CopyPluralFormsExtractorTests(ExtractorTests): found inside a {% trans %} tag and also in another file inside a {% blocktrans %} with a plural (#17375). """ - os.chdir(self.test_dir) management.call_command('makemessages', locale=[LOCALE], extensions=['html', 'djtpl'], verbosity=0) self.assertTrue(os.path.exists(self.PO_FILE)) with open(self.PO_FILE, 'r') as fp: @@ -607,7 +531,6 @@ class CopyPluralFormsExtractorTests(ExtractorTests): class NoWrapExtractorTests(ExtractorTests): def test_no_wrap_enabled(self): - os.chdir(self.test_dir) management.call_command('makemessages', locale=[LOCALE], verbosity=0, no_wrap=True) self.assertTrue(os.path.exists(self.PO_FILE)) with open(self.PO_FILE, 'r') as fp: @@ -619,7 +542,6 @@ class NoWrapExtractorTests(ExtractorTests): ) def test_no_wrap_disabled(self): - os.chdir(self.test_dir) management.call_command('makemessages', locale=[LOCALE], verbosity=0, no_wrap=False) self.assertTrue(os.path.exists(self.PO_FILE)) with open(self.PO_FILE, 'r') as fp: @@ -636,14 +558,12 @@ class LocationCommentsTests(ExtractorTests): def test_no_location_enabled(self): """Behavior is correct if --no-location switch is specified. See #16903.""" - os.chdir(self.test_dir) management.call_command('makemessages', locale=[LOCALE], verbosity=0, no_location=True) self.assertTrue(os.path.exists(self.PO_FILE)) self.assertLocationCommentNotPresent(self.PO_FILE, None, 'test.html') def test_no_location_disabled(self): """Behavior is correct if --no-location switch isn't specified.""" - os.chdir(self.test_dir) management.call_command('makemessages', locale=[LOCALE], verbosity=0, no_location=False) self.assertTrue(os.path.exists(self.PO_FILE)) # #16903 -- Standard comment with source file relative path should be present @@ -654,7 +574,6 @@ class LocationCommentsTests(ExtractorTests): Ensure no leaky paths in comments, e.g. #: path\to\file.html.py:123 Refs #21209/#26341. """ - os.chdir(self.test_dir) management.call_command('makemessages', locale=[LOCALE], verbosity=0) self.assertTrue(os.path.exists(self.PO_FILE)) with open(self.PO_FILE, 'r') as fp: @@ -668,28 +587,16 @@ class KeepPotFileExtractorTests(ExtractorTests): POT_FILE = 'locale/django.pot' - def tearDown(self): - super(KeepPotFileExtractorTests, self).tearDown() - os.chdir(self.test_dir) - try: - os.unlink(self.POT_FILE) - except OSError: - pass - os.chdir(self._cwd) - def test_keep_pot_disabled_by_default(self): - os.chdir(self.test_dir) management.call_command('makemessages', locale=[LOCALE], verbosity=0) self.assertFalse(os.path.exists(self.POT_FILE)) def test_keep_pot_explicitly_disabled(self): - os.chdir(self.test_dir) management.call_command('makemessages', locale=[LOCALE], verbosity=0, keep_pot=False) self.assertFalse(os.path.exists(self.POT_FILE)) def test_keep_pot_enabled(self): - os.chdir(self.test_dir) management.call_command('makemessages', locale=[LOCALE], verbosity=0, keep_pot=True) self.assertTrue(os.path.exists(self.POT_FILE)) @@ -700,18 +607,7 @@ class MultipleLocaleExtractionTests(ExtractorTests): PO_FILE_DE = 'locale/de/LC_MESSAGES/django.po' LOCALES = ['pt', 'de', 'ch'] - def tearDown(self): - super(MultipleLocaleExtractionTests, self).tearDown() - os.chdir(self.test_dir) - for locale in self.LOCALES: - try: - self._rmrf('locale/%s' % locale) - except OSError: - pass - os.chdir(self._cwd) - def test_multiple_locales(self): - os.chdir(self.test_dir) management.call_command('makemessages', locale=['pt', 'de'], verbosity=0) self.assertTrue(os.path.exists(self.PO_FILE_PT)) self.assertTrue(os.path.exists(self.PO_FILE_DE)) @@ -733,10 +629,8 @@ class ExcludedLocaleExtractionTests(ExtractorTests): def setUp(self): super(ExcludedLocaleExtractionTests, self).setUp() - os.chdir(self.test_dir) # ExtractorTests.tearDown() takes care of restoring. - shutil.copytree('canned_locale', 'locale') + copytree('canned_locale', 'locale') self._set_times_for_all_po_files() - self.addCleanup(self._rmrf, os.path.join(self.test_dir, 'locale')) def test_command_help(self): with captured_stdout(), captured_stderr(): @@ -776,7 +670,6 @@ class CustomLayoutExtractionTests(ExtractorTests): work_subdir = 'project_dir' def test_no_locale_raises(self): - os.chdir(self.test_dir) msg = "Unable to find a locale path to store translations for file" with self.assertRaisesMessage(management.CommandError, msg): management.call_command('makemessages', locale=LOCALE, verbosity=0) @@ -788,10 +681,6 @@ class CustomLayoutExtractionTests(ExtractorTests): * translations outside of that app are in LOCALE_PATHS[0] """ with override_settings(LOCALE_PATHS=[os.path.join(self.test_dir, 'project_locale')]): - os.chdir(self.test_dir) - self.addCleanup(shutil.rmtree, os.path.join(settings.LOCALE_PATHS[0], LOCALE), True) - self.addCleanup(shutil.rmtree, os.path.join(self.test_dir, 'app_with_locale', 'locale', LOCALE), True) - management.call_command('makemessages', locale=[LOCALE], verbosity=0) project_de_locale = os.path.join( self.test_dir, 'project_locale', 'de', 'LC_MESSAGES', 'django.po') diff --git a/tests/i18n/test_percents.py b/tests/i18n/test_percents.py index af60df45774..8ce4a0b3287 100644 --- a/tests/i18n/test_percents.py +++ b/tests/i18n/test_percents.py @@ -9,7 +9,8 @@ from django.utils._os import upath from django.utils.encoding import force_text from django.utils.translation import activate, get_language, trans_real -from .test_extraction import POFileAssertionMixin +from .utils import POFileAssertionMixin + SAMPLEPROJECT_DIR = os.path.join(os.path.dirname(os.path.abspath(upath(__file__))), 'sampleproject') SAMPLEPROJECT_LOCALE = os.path.join(SAMPLEPROJECT_DIR, 'locale') diff --git a/tests/i18n/utils.py b/tests/i18n/utils.py new file mode 100644 index 00000000000..4fac02f9bc6 --- /dev/null +++ b/tests/i18n/utils.py @@ -0,0 +1,67 @@ +import os +import re +import shutil +import tempfile + +from django.utils._os import upath + + +source_code_dir = os.path.dirname(upath(__file__)) + + +def copytree(src, dst): + shutil.copytree(src, dst, ignore=shutil.ignore_patterns('*.pyc', '__pycache__')) + + +class POFileAssertionMixin(object): + + def _assertPoKeyword(self, keyword, expected_value, haystack, use_quotes=True): + q = '"' + if use_quotes: + expected_value = '"%s"' % expected_value + q = "'" + needle = '%s %s' % (keyword, expected_value) + expected_value = re.escape(expected_value) + return self.assertTrue( + re.search('^%s %s' % (keyword, expected_value), haystack, re.MULTILINE), + 'Could not find %(q)s%(n)s%(q)s in generated PO file' % {'n': needle, 'q': q} + ) + + def assertMsgId(self, msgid, haystack, use_quotes=True): + return self._assertPoKeyword('msgid', msgid, haystack, use_quotes=use_quotes) + + +class RunInTmpDirMixin(object): + """ + Allow i18n tests that need to generate .po/.mo files to run in an isolated + temporary filesystem tree created by tempfile.mkdtemp() that contains a + clean copy of the relevant test code. + + Test classes using this mixin need to define a `work_subdir` attribute + which designates the subdir under `tests/i18n/` that will be copied to the + temporary tree from which its test cases will run. + + The setUp() method sets the current working dir to the temporary tree. + It'll be removed when cleaning up. + """ + + def setUp(self): + self._cwd = os.getcwd() + self.work_dir = tempfile.mkdtemp(prefix='i18n_') + self.test_dir = os.path.abspath(os.path.join(self.work_dir, self.work_subdir)) + copytree(os.path.join(source_code_dir, self.work_subdir), self.test_dir) + # Step out of the temporary working tree before removing it to avoid + # deletion problems on Windows. Cleanup actions registered with + # addCleanup() are called in reverse so preserve this ordering. + self.addCleanup(self._rmrf, self.test_dir) + self.addCleanup(os.chdir, self._cwd) + os.chdir(self.test_dir) + + def _rmrf(self, dname): + if os.path.commonprefix([self.test_dir, os.path.abspath(dname)]) != self.test_dir: + return + shutil.rmtree(dname) + + def rmfile(self, filepath): + if os.path.exists(filepath): + os.remove(filepath)