Skip to content

Commit b5fbe7f

Browse files
committed
[cmpcodesize] Extract otool subprocess calls
The functions in `cmpcodesize.compare` do several things: they call otool, they use regex matchers to extract information from otool's output, and they output that information using `print()`. Currently, the only way to test cmpcodesize is via functional tests: ones that actually run on a dylib like libswiftCore.dylib. This takes a lot of time and is difficult to fully automate. By extracting otool calls from `cmpcodesize.compare`, we can test those in isolation. Furthermore, future commits can test the functions in `cmpcodesize.compare` using canned strings, instead of actual otool output.
1 parent f436764 commit b5fbe7f

File tree

3 files changed

+126
-18
lines changed

3 files changed

+126
-18
lines changed

utils/cmpcodesize/cmpcodesize/compare.py

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import re
22
import os
3-
import subprocess
43
import collections
54
from operator import itemgetter
65

6+
from cmpcodesize import otool
7+
78
Prefixes = {
89
# Cpp
910
"__Z" : "CPP",
@@ -64,19 +65,10 @@ def addFunction(sizes, function, startAddr, endAddr, groupByPrefix):
6465
sizes[function] += size
6566

6667

67-
def flatten(*args):
68-
for x in args:
69-
if hasattr(x, '__iter__'):
70-
for y in flatten(*x):
71-
yield y
72-
else:
73-
yield x
74-
75-
7668
def readSizes(sizes, fileName, functionDetails, groupByPrefix):
7769
# Check if multiple architectures are supported by the object file.
7870
# Prefer arm64 if available.
79-
architectures = subprocess.check_output(["otool", "-V", "-f", fileName]).split("\n")
71+
architectures = otool.fat_headers(fileName).split('\n')
8072
arch = None
8173
archPattern = re.compile('architecture ([\S]+)')
8274
for architecture in architectures:
@@ -86,16 +78,14 @@ def readSizes(sizes, fileName, functionDetails, groupByPrefix):
8678
arch = archMatch.group(1)
8779
if "arm64" in arch:
8880
arch = "arm64"
89-
if arch is not None:
90-
archParams = ["-arch", arch]
91-
else:
92-
archParams = []
9381

9482
if functionDetails:
95-
content = subprocess.check_output(flatten(["otool", archParams, "-l", "-v", "-t", fileName])).split("\n")
96-
content += subprocess.check_output(flatten(["otool", archParams, "-v", "-s", "__TEXT", "__textcoal_nt", fileName])).split("\n")
83+
content = otool.load_commands(fileName,
84+
architecture=arch,
85+
include_text_sections=True).split('\n')
86+
content += otool.text_sections(fileName, architecture=arch).split('\n')
9787
else:
98-
content = subprocess.check_output(flatten(["otool", archParams, "-l", fileName])).split("\n")
88+
content = otool.load_commands(fileName, architecture=arch).split('\n')
9989

10090
sectName = None
10191
currFunc = None
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import subprocess
2+
3+
4+
def _command_for_architecture(architecture=None):
5+
if architecture is None:
6+
return ['otool']
7+
else:
8+
return ['otool', '-arch', architecture]
9+
10+
11+
def fat_headers(path):
12+
"""
13+
Returns the headers for the executable at the given path.
14+
Raises a subprocess.CalledProcessError if otool encounters an error,
15+
such as not finding a file at the given path.
16+
"""
17+
return subprocess.check_output(['otool', '-V', '-f', path])
18+
19+
20+
def load_commands(path, architecture=None, include_text_sections=False):
21+
"""
22+
Returns the load commands for the executable at the given path,
23+
for the given architecture. If print_text_section is specified,
24+
the disassembled text section of the load commands is also output.
25+
26+
Raises a subprocess.CalledProcessError if otool encounters an error,
27+
such as not finding a file at the given path.
28+
"""
29+
command = _command_for_architecture(architecture) + ['-l']
30+
if include_text_sections:
31+
command += ['-v', '-t']
32+
return subprocess.check_output(command + [path])
33+
34+
35+
def text_sections(path, architecture=None):
36+
"""
37+
Returns the contents of the text sections of the executable at the
38+
given path, for the given architecture.
39+
40+
Raises a subprocess.CalledProcessError if otool encounters an error,
41+
such as not finding a file at the given path.
42+
"""
43+
return subprocess.check_output(
44+
_command_for_architecture(architecture) +
45+
['-v', '-s', '__TEXT', '__textcoal_nt', path])

utils/cmpcodesize/tests/test_otool.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import subprocess
2+
import unittest
3+
4+
from cmpcodesize import otool
5+
6+
7+
# Store parameters passed to subprocess.check_output into
8+
# this global variable.
9+
_subprocess_check_output_arguments = []
10+
11+
12+
# We'll monkey-patch subprocess.check_output with this stub
13+
# function, which simply records whatever's passed to
14+
# check_output into the global variables above.
15+
def _stub_subprocess_check_output(arguments, *args, **kwargs):
16+
global _subprocess_check_output_arguments
17+
_subprocess_check_output_arguments = arguments
18+
19+
20+
class OtoolTestCase(unittest.TestCase):
21+
def setUp(self):
22+
# Monkey-patch subprocess.check_output with our stub function.
23+
self._original_check_output = subprocess.check_output
24+
subprocess.check_output = _stub_subprocess_check_output
25+
26+
def tearDown(self):
27+
# Undo the monkey-patching.
28+
subprocess.check_output = self._original_check_output
29+
30+
def test_fat_headers(self):
31+
otool.fat_headers('/path/to/foo')
32+
self.assertEqual(_subprocess_check_output_arguments,
33+
['otool', '-V', '-f', '/path/to/foo'])
34+
35+
def test_load_commands_with_no_architecture(self):
36+
otool.load_commands('/path/to/bar')
37+
self.assertEqual(_subprocess_check_output_arguments,
38+
['otool', '-l', '/path/to/bar'])
39+
40+
def test_load_commands_with_architecture(self):
41+
otool.load_commands('/path/to/baz', architecture='arch-foo')
42+
self.assertEqual(
43+
_subprocess_check_output_arguments,
44+
['otool', '-arch', 'arch-foo', '-l', '/path/to/baz'])
45+
46+
def test_load_commands_no_architecture_but_including_text_sections(self):
47+
otool.load_commands(
48+
'/path/to/flim', include_text_sections=True)
49+
self.assertEqual(
50+
_subprocess_check_output_arguments,
51+
['otool', '-l', '-v', '-t', '/path/to/flim'])
52+
53+
def test_load_commands_with_architecture_and_including_text_sections(self):
54+
otool.load_commands(
55+
'/path/to/flam',
56+
architecture='arch-bar',
57+
include_text_sections=True)
58+
self.assertEqual(
59+
_subprocess_check_output_arguments,
60+
['otool', '-arch', 'arch-bar', '-l', '-v', '-t', '/path/to/flam'])
61+
62+
def test_text_sections_no_architecture(self):
63+
otool.text_sections('/path/to/fish')
64+
self.assertEqual(
65+
_subprocess_check_output_arguments,
66+
['otool', '-v', '-s', '__TEXT', '__textcoal_nt', '/path/to/fish'])
67+
68+
def test_text_sections_with_architecture(self):
69+
otool.text_sections('/path/to/frosh', architecture='arch-baz')
70+
self.assertEqual(
71+
_subprocess_check_output_arguments,
72+
['otool', '-arch', 'arch-baz', '-v', '-s',
73+
'__TEXT', '__textcoal_nt', '/path/to/frosh'])

0 commit comments

Comments
 (0)