Skip to content

Commit fbf8b3f

Browse files
test: Fix test failures & add Windows CI tests (#206)
1 parent df5ef40 commit fbf8b3f

File tree

4 files changed

+110
-37
lines changed

4 files changed

+110
-37
lines changed

.github/workflows/ci.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ jobs:
2323
- run: python -m flake8
2424
- run: python -m mypy fluent.syntax/fluent fluent.runtime/fluent
2525
test:
26-
runs-on: ubuntu-latest
26+
runs-on: ${{ matrix.os }}
2727
strategy:
2828
matrix:
29+
os: [ubuntu-22.04, windows-2022]
2930
python-version: [3.7, 3.8, 3.9, "3.10", 3.11, 3.12, pypy3.9, pypy3.10]
3031
steps:
3132
- uses: actions/checkout@v4

fluent.runtime/tests/test_fallback.py

+21-36
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
1-
import io
2-
import os
31
import unittest
4-
from unittest import mock
2+
from .utils import patch_files
53

64
from fluent.runtime import FluentLocalization, FluentResourceLoader
75

8-
ISFILE = os.path.isfile
9-
106

117
class TestLocalization(unittest.TestCase):
128
def test_init(self):
@@ -15,21 +11,18 @@ def test_init(self):
1511
)
1612
self.assertTrue(callable(l10n.format_value))
1713

18-
@mock.patch("os.path.isfile")
19-
@mock.patch("codecs.open")
20-
def test_bundles(self, codecs_open, isfile):
21-
data = {
22-
"de/one.ftl": "one = in German",
23-
"de/two.ftl": "two = in German",
24-
"fr/two.ftl": "three = in French",
25-
"en/one.ftl": "four = exists",
26-
"en/two.ftl": "five = exists",
27-
}
28-
isfile.side_effect = lambda p: p in data or ISFILE(p)
29-
codecs_open.side_effect = lambda p, _, __: io.StringIO(data[p])
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):
3022
l10n = FluentLocalization(
3123
["de", "fr", "en"], ["one.ftl", "two.ftl"], FluentResourceLoader("{locale}")
3224
)
25+
# Curious
3326
bundles_gen = l10n._bundles()
3427
bundle_de = next(bundles_gen)
3528
self.assertEqual(bundle_de.locales[0], "de")
@@ -49,38 +42,30 @@ def test_bundles(self, codecs_open, isfile):
4942
self.assertEqual(l10n.format_value("five"), "exists")
5043

5144

52-
@mock.patch("os.path.isfile")
53-
@mock.patch("codecs.open")
5445
class TestResourceLoader(unittest.TestCase):
55-
def test_all_exist(self, codecs_open, isfile):
56-
data = {
57-
"en/one.ftl": "one = exists",
58-
"en/two.ftl": "two = exists",
59-
}
60-
isfile.side_effect = lambda p: p in data
61-
codecs_open.side_effect = lambda p, _, __: io.StringIO(data[p])
46+
@patch_files({
47+
"en/one.ftl": "one = exists",
48+
"en/two.ftl": "two = exists",
49+
})
50+
def test_all_exist(self):
6251
loader = FluentResourceLoader("{locale}")
6352
resources_list = list(loader.resources("en", ["one.ftl", "two.ftl"]))
6453
self.assertEqual(len(resources_list), 1)
6554
resources = resources_list[0]
6655
self.assertEqual(len(resources), 2)
6756

68-
def test_one_exists(self, codecs_open, isfile):
69-
data = {
70-
"en/two.ftl": "two = exists",
71-
}
72-
isfile.side_effect = lambda p: p in data
73-
codecs_open.side_effect = lambda p, _, __: io.StringIO(data[p])
57+
@patch_files({
58+
"en/two.ftl": "two = exists",
59+
})
60+
def test_one_exists(self):
7461
loader = FluentResourceLoader("{locale}")
7562
resources_list = list(loader.resources("en", ["one.ftl", "two.ftl"]))
7663
self.assertEqual(len(resources_list), 1)
7764
resources = resources_list[0]
7865
self.assertEqual(len(resources), 1)
7966

80-
def test_none_exist(self, codecs_open, isfile):
81-
data = {}
82-
isfile.side_effect = lambda p: p in data
83-
codecs_open.side_effect = lambda p, _, __: io.StringIO(data[p])
67+
@patch_files({})
68+
def test_none_exist(self):
8469
loader = FluentResourceLoader("{locale}")
8570
resources_list = list(loader.resources("en", ["one.ftl", "two.ftl"]))
8671
self.assertEqual(len(resources_list), 0)

fluent.runtime/tests/test_utils.py

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
26+
with self.assertRaises(ValueError):
27+
os.path.isfile("en/")
28+
patch_me(10, "b")
29+
30+
def assertFileIs(self, filename, expect_contents):
31+
"""
32+
expect_contents is None: Expect file does not exist
33+
expect_contents is a str: Expect file to exist and contents to match
34+
"""
35+
if expect_contents is None:
36+
self.assertFalse(os.path.isfile(filename),
37+
f"Expected {filename} to not exist.")
38+
else:
39+
self.assertTrue(os.path.isfile(filename),
40+
f"Expected {filename} to exist.")
41+
with codecs.open(filename, "r", "utf-8") as f:
42+
self.assertEqual(f.read(), expect_contents)

fluent.runtime/tests/utils.py

+45
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,50 @@
1+
"""Utilities for testing."""
2+
13
import textwrap
4+
from pathlib import PureWindowsPath, PurePosixPath
5+
from unittest import mock
6+
from io import StringIO
7+
import functools
28

39

410
def dedent_ftl(text):
511
return textwrap.dedent(f"{text.rstrip()}\n")
12+
13+
14+
# Needed in test_falllback.py because it uses dict + string compare to make a virtual file structure
15+
def _normalize_file_path(path):
16+
"""Note: Does not support absolute paths or paths that
17+
contain '.' or '..' parts."""
18+
# Cannot use os.path or PurePath, because they only recognize
19+
# one kind of path separator
20+
if PureWindowsPath(path).is_absolute() or PurePosixPath(path).is_absolute():
21+
raise ValueError(f"Unsupported path: {path}")
22+
parts = path.replace("\\", "/").split("/")
23+
if "." in parts or ".." in parts:
24+
raise ValueError(f"Unsupported path: {path}")
25+
if parts and parts[-1] == "":
26+
# path ends with a trailing pathsep
27+
raise ValueError(f"Path appears to be a directory, not a file: {path}")
28+
return "/".join(parts)
29+
30+
31+
def patch_files(files: dict):
32+
"""Decorate a function to simulate files ``files`` during the function.
33+
34+
The keys of ``files`` are file names and must use '/' for path separator.
35+
The values are file contents. Directories or relative paths are not supported.
36+
Example: ``{"en/one.txt": "One", "en/two.txt": "Two"}``
37+
38+
The implementation may be changed to match the mechanism used.
39+
"""
40+
41+
# Here it is possible to validate file names, but skipped
42+
43+
def then(func):
44+
@mock.patch("os.path.isfile", side_effect=lambda p: _normalize_file_path(p) in files)
45+
@mock.patch("codecs.open", side_effect=lambda p, _, __: StringIO(files[_normalize_file_path(p)]))
46+
@functools.wraps(func) # Make ret look like func to later decorators
47+
def ret(*args, **kwargs):
48+
func(*args[:-2], **kwargs)
49+
return ret
50+
return then

0 commit comments

Comments
 (0)