Skip to content

Commit 50bd109

Browse files
committed
Merge branch 'mp-saveXML'
2 parents 134a3e0 + aaecd3d commit 50bd109

File tree

4 files changed

+133
-16
lines changed

4 files changed

+133
-16
lines changed

lib/fdiff/__main__.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ def run(argv):
5959
)
6060
parser.add_argument("--head", type=int, help="Display first n lines of output")
6161
parser.add_argument("--tail", type=int, help="Display last n lines of output")
62+
parser.add_argument(
63+
"--nomp", action="store_true", help="Do not use multi process optimizations"
64+
)
6265
parser.add_argument("PREFILE", help="Font file path 1")
6366
parser.add_argument("POSTFILE", help="Font file path 2")
6467

@@ -113,6 +116,10 @@ def run(argv):
113116
include_list = get_tables_argument_list(args.include)
114117
exclude_list = get_tables_argument_list(args.exclude)
115118

119+
# flip logic of the command line flag for multi process
120+
# optimizations for use as a u_diff function argument
121+
use_mp = not args.nomp
122+
116123
# perform the unified diff analysis
117124
try:
118125
diff = u_diff(
@@ -121,6 +128,7 @@ def run(argv):
121128
context_lines=args.lines,
122129
include_tables=include_list,
123130
exclude_tables=exclude_list,
131+
use_multiprocess=use_mp,
124132
)
125133
except Exception as e:
126134
sys.stderr.write(f"[*] ERROR: {e}{os.linesep}")
@@ -136,8 +144,17 @@ def run(argv):
136144
iterable = diff
137145

138146
# print unified diff results to standard output stream
147+
has_diff = False
139148
if args.color:
140149
for line in iterable:
150+
has_diff = True
141151
sys.stdout.write(color_unified_diff_line(line))
142152
else:
143-
sys.stdout.writelines(iterable)
153+
for line in iterable:
154+
has_diff = True
155+
sys.stdout.write(line)
156+
157+
# if no difference was found, tell the user instead of
158+
# simply closing with zero exit status code.
159+
if not has_diff:
160+
print("[*] There is no difference between the files.")

lib/fdiff/diff.py

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import os
3+
from multiprocessing import Pool, cpu_count
34
import tempfile
45

56
from fontTools.ttLib import TTFont
@@ -13,8 +14,19 @@
1314
from fdiff.utils import get_file_modtime
1415

1516

17+
def _ttfont_save_xml(ttf, filepath, include_tables, exclude_tables):
18+
"""Writes TTX specification formatted XML to disk on filepath."""
19+
ttf.saveXML(filepath, tables=include_tables, skipTables=exclude_tables)
20+
return True
21+
22+
1623
def u_diff(
17-
filepath_a, filepath_b, context_lines=3, include_tables=None, exclude_tables=None
24+
filepath_a,
25+
filepath_b,
26+
context_lines=3,
27+
include_tables=None,
28+
exclude_tables=None,
29+
use_multiprocess=True,
1830
):
1931
"""Performs a unified diff on a TTX serialized data format dump of font binary data using
2032
a modified version of the Python standard libary difflib module.
@@ -105,20 +117,30 @@ def u_diff(
105117
fromdate = get_file_modtime(prepath)
106118
todate = get_file_modtime(postpath)
107119

108-
tt_left.saveXML(
109-
os.path.join(tmpdirname, "left.ttx"),
110-
tables=include_tables,
111-
skipTables=exclude_tables,
112-
)
113-
tt_right.saveXML(
114-
os.path.join(tmpdirname, "right.ttx"),
115-
tables=include_tables,
116-
skipTables=exclude_tables,
117-
)
120+
left_ttxpath = os.path.join(tmpdirname, "left.ttx")
121+
right_ttxpath = os.path.join(tmpdirname, "right.ttx")
122+
123+
if use_multiprocess and cpu_count() > 1:
124+
# Use parallel fontTools.ttLib.TTFont.saveXML dump
125+
# by default on multi CPU systems. This is a performance
126+
# optimization. Profiling demonstrates that this can reduce
127+
# execution time by up to 30% for some fonts
128+
mp_args_list = [
129+
(tt_left, left_ttxpath, include_tables, exclude_tables),
130+
(tt_right, right_ttxpath, include_tables, exclude_tables),
131+
]
132+
with Pool(processes=2) as pool:
133+
pool.starmap(_ttfont_save_xml, mp_args_list)
134+
else:
135+
# use sequential fontTools.ttLib.TTFont.saveXML dumps
136+
# when use_multiprocess is False or single CPU system
137+
# detected
138+
_ttfont_save_xml(tt_left, left_ttxpath, include_tables, exclude_tables)
139+
_ttfont_save_xml(tt_right, right_ttxpath, include_tables, exclude_tables)
118140

119-
with open(os.path.join(tmpdirname, "left.ttx")) as ff:
141+
with open(left_ttxpath) as ff:
120142
fromlines = ff.readlines()
121-
with open(os.path.join(tmpdirname, "right.ttx")) as tf:
143+
with open(right_ttxpath) as tf:
122144
tolines = tf.readlines()
123145

124146
return unified_diff(

tests/test_diff.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import os
2+
import tempfile
23

34
import pytest
45

5-
from fdiff.diff import u_diff
6+
from fontTools.ttLib import TTFont
7+
8+
from fdiff.diff import u_diff, _ttfont_save_xml
69
from fdiff.exceptions import AIOError
710

811
ROBOTO_BEFORE_PATH = os.path.join("tests", "testfiles", "Roboto-Regular.subset1.ttf")
@@ -44,6 +47,12 @@
4447
ROBOTO_UDIFF_EXCLUDE_HEADPOST_EXPECTED = robo_udiff_ex_headpost.read()
4548

4649

50+
def test_unified_diff_default_no_diff():
51+
res = u_diff(ROBOTO_BEFORE_PATH, ROBOTO_BEFORE_PATH)
52+
res_string = "".join(res)
53+
assert res_string == ""
54+
55+
4756
def test_unified_diff_default():
4857
res = u_diff(ROBOTO_BEFORE_PATH, ROBOTO_AFTER_PATH)
4958
res_string = "".join(res)
@@ -63,6 +72,25 @@ def test_unified_diff_default():
6372
assert line == expected_string_list[x]
6473

6574

75+
def test_unified_diff_without_mp_optimizations():
76+
res = u_diff(ROBOTO_BEFORE_PATH, ROBOTO_AFTER_PATH, use_multiprocess=False)
77+
res_string = "".join(res)
78+
res_string_list = res_string.split("\n")
79+
expected_string_list = ROBOTO_UDIFF_EXPECTED.split("\n")
80+
81+
# have to handle the tests for the top two file path lines
82+
# differently than the rest of the comparisons because
83+
# the time is defined using local platform settings
84+
# which makes tests fail on remote CI testing services vs.
85+
# my local testing platform...
86+
for x, line in enumerate(res_string_list):
87+
# treat top two lines of the diff as comparison of first 10 chars only
88+
if x in (0, 1):
89+
assert line[0:9] == expected_string_list[x][0:9]
90+
else:
91+
assert line == expected_string_list[x]
92+
93+
6694
def test_unified_diff_context_lines_1():
6795
res = u_diff(ROBOTO_BEFORE_PATH, ROBOTO_AFTER_PATH, context_lines=1)
6896
res_string = "".join(res)
@@ -210,4 +238,24 @@ def test_unified_diff_remote_404_first_file():
210238

211239
def test_unified_diff_remote_404_second_file():
212240
with pytest.raises(AIOError):
213-
u_diff(ROBOTO_BEFORE_URL, URL_404)
241+
u_diff(ROBOTO_BEFORE_URL, URL_404)
242+
243+
244+
def test_unified_diff_remote_non_url_exception():
245+
"""This raises an exception in the aiohttp get request call"""
246+
with pytest.raises(AIOError):
247+
u_diff("https:bogus", "https:bogus")
248+
249+
250+
#
251+
#
252+
# Private functions
253+
#
254+
#
255+
256+
def test_ttfont_save_xml():
257+
tt = TTFont(ROBOTO_BEFORE_PATH)
258+
with tempfile.TemporaryDirectory() as tmpdirname:
259+
path = os.path.join(tmpdirname, "test.ttx")
260+
_ttfont_save_xml(tt, path, None, None)
261+
assert os.path.isfile(path)

tests/test_main.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,15 @@ def test_main_include_exclude_defined_simultaneously(capsys):
9292
# Unified diff integration tests
9393
#
9494

95+
def test_main_run_unified_default_local_files_no_diff(capsys):
96+
"""Test default behavior when there is no difference in font files under evaluation"""
97+
args = [ROBOTO_BEFORE_PATH, ROBOTO_BEFORE_PATH]
98+
99+
run(args)
100+
captured = capsys.readouterr()
101+
assert captured.out.startswith("[*] There is no difference between the files.")
102+
103+
95104
def test_main_run_unified_default_local_files(capsys):
96105
args = [ROBOTO_BEFORE_PATH, ROBOTO_AFTER_PATH]
97106

@@ -113,6 +122,27 @@ def test_main_run_unified_default_local_files(capsys):
113122
assert line == expected_string_list[x]
114123

115124

125+
def test_main_run_unified_local_files_without_mp_optimizations(capsys):
126+
args = ["--nomp", ROBOTO_BEFORE_PATH, ROBOTO_AFTER_PATH]
127+
128+
run(args)
129+
captured = capsys.readouterr()
130+
131+
res_string_list = captured.out.split("\n")
132+
expected_string_list = ROBOTO_UDIFF_EXPECTED.split("\n")
133+
134+
# have to handle the tests for the top two file path lines
135+
# differently than the rest of the comparisons because
136+
# the time is defined using local platform settings
137+
# which makes tests fail on different remote CI testing services
138+
for x, line in enumerate(res_string_list):
139+
# treat top two lines of the diff as comparison of first 10 chars only
140+
if x in (0, 1):
141+
assert line[0:9] == expected_string_list[x][0:9]
142+
else:
143+
assert line == expected_string_list[x]
144+
145+
116146
def test_main_run_unified_default_remote_files(capsys):
117147
args = [ROBOTO_BEFORE_URL, ROBOTO_AFTER_URL]
118148

0 commit comments

Comments
 (0)