Skip to content

Commit 457d698

Browse files
committed
Refactor & add tests for file simulating code
1 parent 3cf815f commit 457d698

File tree

4 files changed

+115
-46
lines changed

4 files changed

+115
-46
lines changed

fluent.runtime/tests/__init__.py

-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +0,0 @@
1-
import os
2-
3-
4-
# Unify path separator, default path separator on Windows is \ not /
5-
# Needed in test_falllback.py because it uses dict + string compare to make a virtual file structure
6-
def normalize_path(path):
7-
return "/".join(os.path.split(path))

fluent.runtime/tests/test_fallback.py

+20-39
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,8 @@
1-
import io
2-
import os
31
import unittest
4-
from unittest import mock
5-
6-
from . import normalize_path
2+
from .utils import patch_files
73

84
from fluent.runtime import FluentLocalization, FluentResourceLoader
95

10-
ISFILE = os.path.isfile
11-
12-
13-
def patch_io(codecs_open, isfile, data):
14-
isfile.side_effect = lambda p: normalize_path(p) in data or ISFILE(p)
15-
codecs_open.side_effect = lambda p, _, __: io.StringIO(data[normalize_path(p)])
16-
176

187
class TestLocalization(unittest.TestCase):
198
def test_init(self):
@@ -22,17 +11,14 @@ def test_init(self):
2211
)
2312
self.assertTrue(callable(l10n.format_value))
2413

25-
@mock.patch("os.path.isfile")
26-
@mock.patch("codecs.open")
27-
def test_bundles(self, codecs_open, isfile):
28-
data = {
29-
"de/one.ftl": "one = in German",
30-
"de/two.ftl": "two = in German",
31-
"fr/two.ftl": "three = in French",
32-
"en/one.ftl": "four = exists",
33-
"en/two.ftl": "five = exists",
34-
}
35-
patch_io(codecs_open, isfile, data)
14+
@patch_files({
15+
"de/one.ftl": "one = in German",
16+
"de/two.ftl": "two = in German",
17+
"fr/two.ftl": "three = in French",
18+
"en/one.ftl": "four = exists",
19+
"en/two.ftl": "five = exists",
20+
})
21+
def test_bundles(self):
3622
l10n = FluentLocalization(
3723
["de", "fr", "en"], ["one.ftl", "two.ftl"], FluentResourceLoader("{locale}")
3824
)
@@ -56,35 +42,30 @@ def test_bundles(self, codecs_open, isfile):
5642
self.assertEqual(l10n.format_value("five"), "exists")
5743

5844

59-
@mock.patch("os.path.isfile")
60-
@mock.patch("codecs.open")
6145
class TestResourceLoader(unittest.TestCase):
62-
def test_all_exist(self, codecs_open, isfile):
63-
data = {
64-
"en/one.ftl": "one = exists",
65-
"en/two.ftl": "two = exists",
66-
}
67-
patch_io(codecs_open, isfile, data)
46+
@patch_files({
47+
"en/one.ftl": "one = exists",
48+
"en/two.ftl": "two = exists",
49+
})
50+
def test_all_exist(self):
6851
loader = FluentResourceLoader("{locale}")
6952
resources_list = list(loader.resources("en", ["one.ftl", "two.ftl"]))
7053
self.assertEqual(len(resources_list), 1)
7154
resources = resources_list[0]
7255
self.assertEqual(len(resources), 2)
7356

74-
def test_one_exists(self, codecs_open, isfile):
75-
data = {
76-
"en/two.ftl": "two = exists",
77-
}
78-
patch_io(codecs_open, isfile, data)
57+
@patch_files({
58+
"en/two.ftl": "two = exists",
59+
})
60+
def test_one_exists(self):
7961
loader = FluentResourceLoader("{locale}")
8062
resources_list = list(loader.resources("en", ["one.ftl", "two.ftl"]))
8163
self.assertEqual(len(resources_list), 1)
8264
resources = resources_list[0]
8365
self.assertEqual(len(resources), 1)
8466

85-
def test_none_exist(self, codecs_open, isfile):
86-
data = {}
87-
patch_io(codecs_open, isfile, data)
67+
@patch_files({})
68+
def test_none_exist(self):
8869
loader = FluentResourceLoader("{locale}")
8970
resources_list = list(loader.resources("en", ["one.ftl", "two.ftl"]))
9071
self.assertEqual(len(resources_list), 0)

fluent.runtime/tests/test_utils.py

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import unittest
2+
from .utils import patch_files
3+
import os
4+
import codecs
5+
6+
7+
class TestFileSimulate(unittest.TestCase):
8+
def test_basic(self):
9+
@patch_files({
10+
"the.txt": "The",
11+
"en/one.txt": "One",
12+
"en/two.txt": "Two"
13+
})
14+
def patch_me(a, b):
15+
self.assertEqual(a, 10)
16+
self.assertEqual(b, "b")
17+
self.assertFileIs(os.path.basename(__file__), None)
18+
self.assertFileIs("the.txt", "The")
19+
self.assertFileIs("en/one.txt", "One")
20+
self.assertFileIs("en\\one.txt", "One")
21+
self.assertFileIs("en/two.txt", "Two")
22+
self.assertFileIs("en\\two.txt", "Two")
23+
self.assertFileIs("en/three.txt", None)
24+
self.assertFileIs("en\\three.txt", None)
25+
patch_me(10, "b")
26+
27+
def assertFileIs(self, filename, expect_contents):
28+
"""
29+
expect_contents is None: Expect file does not exist
30+
expect_contents is a str: Expect file contents to match
31+
"""
32+
if expect_contents is None:
33+
self.assertFalse(os.path.isfile(filename),
34+
"Expected " + filename + " to not exist.")
35+
else:
36+
self.assertTrue(os.path.isfile(filename),
37+
"Expected " + filename + " to exist.")
38+
self.assertEqual(codecs.open(filename, "r", "utf-8").read(),
39+
expect_contents)

fluent.runtime/tests/utils.py

+56
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,61 @@
1+
"""Utilities for testing.
2+
3+
Import this module before patching libraries.
4+
"""
5+
16
import textwrap
7+
from pathlib import PurePath
8+
from unittest import mock
9+
from io import StringIO
10+
import functools
211

312

413
def dedent_ftl(text):
514
return textwrap.dedent(f"{text.rstrip()}\n")
15+
16+
17+
# Unify path separator, default path separator on Windows is \ not /
18+
# Supports only relative paths
19+
# Needed in test_falllback.py because it uses dict + string compare to make a virtual file structure
20+
def _normalize_path(path):
21+
path = PurePath(path)
22+
if path.is_absolute():
23+
raise ValueError("Absolute paths are not supported in file simulation yet. ("
24+
+ str(path) + ")")
25+
if "." not in path.parts and ".." not in path.parts:
26+
return "/".join(PurePath(path).parts)
27+
else:
28+
res_parts = []
29+
length = len(path.parts)
30+
i = 0
31+
while i < length:
32+
if path.parts[i] == ".":
33+
i += 1
34+
elif i < length - 1 and path.parts[i+1] == "..":
35+
i += 2
36+
else:
37+
res_parts.append(path.parts[i])
38+
i += 1
39+
return "/".join(res_parts)
40+
41+
42+
def patch_files(files: dict):
43+
"""Decorate a function to simulate files ``files`` during the function.
44+
45+
The keys of ``files`` are file names and must use '/' for path separator.
46+
The values are file contents. Directories or relative paths are not supported.
47+
Example: ``{"en/one.txt": "One", "en/two.txt": "Two"}``
48+
49+
The implementation may be changed to match the mechanism used.
50+
"""
51+
if files is None:
52+
files = {}
53+
54+
def then(func):
55+
@mock.patch("os.path.isfile", side_effect=lambda p: _normalize_path(p) in files)
56+
@mock.patch("codecs.open", side_effect=lambda p, _, __: StringIO(files[_normalize_path(p)]))
57+
@functools.wraps(func) # Make ret look like func to later decorators
58+
def ret(*args, **kwargs):
59+
func(*args[:-2], **kwargs)
60+
return ret
61+
return then

0 commit comments

Comments
 (0)