|
1 #!/usr/bin/env python |
|
2 # This Source Code Form is subject to the terms of the Mozilla Public |
|
3 # License, v. 2.0. If a copy of the MPL was not distributed with this |
|
4 # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
|
5 |
|
6 import os, tempfile, unittest, shutil, struct, platform, subprocess, multiprocessing.dummy |
|
7 import mock |
|
8 from mock import patch |
|
9 import symbolstore |
|
10 |
|
11 # Some simple functions to mock out files that the platform-specific dumpers will accept. |
|
12 # dump_syms itself will not be run (we mock that call out), but we can't override |
|
13 # the ShouldProcessFile method since we actually want to test that. |
|
14 def write_elf(filename): |
|
15 open(filename, "wb").write(struct.pack("<7B45x", 0x7f, ord("E"), ord("L"), ord("F"), 1, 1, 1)) |
|
16 |
|
17 def write_macho(filename): |
|
18 open(filename, "wb").write(struct.pack("<I28x", 0xfeedface)) |
|
19 |
|
20 def write_pdb(filename): |
|
21 open(filename, "w").write("aaa") |
|
22 # write out a fake DLL too |
|
23 open(os.path.splitext(filename)[0] + ".dll", "w").write("aaa") |
|
24 |
|
25 writer = {'Windows': write_pdb, |
|
26 'Microsoft': write_pdb, |
|
27 'Linux': write_elf, |
|
28 'Sunos5': write_elf, |
|
29 'Darwin': write_macho}[platform.system()] |
|
30 extension = {'Windows': ".pdb", |
|
31 'Microsoft': ".pdb", |
|
32 'Linux': ".so", |
|
33 'Sunos5': ".so", |
|
34 'Darwin': ".dylib"}[platform.system()] |
|
35 |
|
36 def add_extension(files): |
|
37 return [f + extension for f in files] |
|
38 |
|
39 class HelperMixin(object): |
|
40 """ |
|
41 Test that passing filenames to exclude from processing works. |
|
42 """ |
|
43 def setUp(self): |
|
44 self.test_dir = tempfile.mkdtemp() |
|
45 if not self.test_dir.endswith("/"): |
|
46 self.test_dir += "/" |
|
47 symbolstore.srcdirRepoInfo = {} |
|
48 symbolstore.vcsFileInfoCache = {} |
|
49 |
|
50 def tearDown(self): |
|
51 shutil.rmtree(self.test_dir) |
|
52 symbolstore.srcdirRepoInfo = {} |
|
53 symbolstore.vcsFileInfoCache = {} |
|
54 |
|
55 def add_test_files(self, files): |
|
56 for f in files: |
|
57 f = os.path.join(self.test_dir, f) |
|
58 d = os.path.dirname(f) |
|
59 if d and not os.path.exists(d): |
|
60 os.makedirs(d) |
|
61 writer(f) |
|
62 |
|
63 class TestExclude(HelperMixin, unittest.TestCase): |
|
64 def test_exclude_wildcard(self): |
|
65 """ |
|
66 Test that using an exclude list with a wildcard pattern works. |
|
67 """ |
|
68 processed = [] |
|
69 def mock_process_file(filenames): |
|
70 for filename in filenames: |
|
71 processed.append((filename[len(self.test_dir):] if filename.startswith(self.test_dir) else filename).replace('\\', '/')) |
|
72 return True |
|
73 self.add_test_files(add_extension(["foo", "bar", "abc/xyz", "abc/fooxyz", "def/asdf", "def/xyzfoo"])) |
|
74 d = symbolstore.GetPlatformSpecificDumper(dump_syms="dump_syms", |
|
75 symbol_path="symbol_path", |
|
76 exclude=["*foo*"]) |
|
77 d.ProcessFiles = mock_process_file |
|
78 d.Process(self.test_dir) |
|
79 d.Finish(stop_pool=False) |
|
80 processed.sort() |
|
81 expected = add_extension(["bar", "abc/xyz", "def/asdf"]) |
|
82 expected.sort() |
|
83 self.assertEqual(processed, expected) |
|
84 |
|
85 def test_exclude_filenames(self): |
|
86 """ |
|
87 Test that excluding a filename without a wildcard works. |
|
88 """ |
|
89 processed = [] |
|
90 def mock_process_file(filenames): |
|
91 for filename in filenames: |
|
92 processed.append((filename[len(self.test_dir):] if filename.startswith(self.test_dir) else filename).replace('\\', '/')) |
|
93 return True |
|
94 self.add_test_files(add_extension(["foo", "bar", "abc/foo", "abc/bar", "def/foo", "def/bar"])) |
|
95 d = symbolstore.GetPlatformSpecificDumper(dump_syms="dump_syms", |
|
96 symbol_path="symbol_path", |
|
97 exclude=add_extension(["foo"])) |
|
98 d.ProcessFiles = mock_process_file |
|
99 d.Process(self.test_dir) |
|
100 d.Finish(stop_pool=False) |
|
101 processed.sort() |
|
102 expected = add_extension(["bar", "abc/bar", "def/bar"]) |
|
103 expected.sort() |
|
104 self.assertEqual(processed, expected) |
|
105 |
|
106 def popen_factory(stdouts): |
|
107 """ |
|
108 Generate a class that can mock subprocess.Popen. |stdouts| is an iterable that |
|
109 should return an iterable for the stdout of each process in turn. |
|
110 """ |
|
111 class mock_popen(object): |
|
112 def __init__(self, args, *args_rest, **kwargs): |
|
113 self.stdout = stdouts.next() |
|
114 |
|
115 def wait(self): |
|
116 return 0 |
|
117 return mock_popen |
|
118 |
|
119 def mock_dump_syms(module_id, filename): |
|
120 return ["MODULE os x86 %s %s" % (module_id, filename), |
|
121 "FILE 0 foo.c", |
|
122 "PUBLIC xyz 123"] |
|
123 |
|
124 class TestCopyDebugUniversal(HelperMixin, unittest.TestCase): |
|
125 """ |
|
126 Test that CopyDebug does the right thing when dumping multiple architectures. |
|
127 """ |
|
128 def setUp(self): |
|
129 HelperMixin.setUp(self) |
|
130 self.symbol_dir = tempfile.mkdtemp() |
|
131 self._subprocess_call = subprocess.call |
|
132 subprocess.call = self.mock_call |
|
133 self._subprocess_popen = subprocess.Popen |
|
134 subprocess.Popen = popen_factory(self.next_mock_stdout()) |
|
135 self.stdouts = [] |
|
136 self._shutil_rmtree = shutil.rmtree |
|
137 shutil.rmtree = self.mock_rmtree |
|
138 |
|
139 def tearDown(self): |
|
140 HelperMixin.tearDown(self) |
|
141 shutil.rmtree = self._shutil_rmtree |
|
142 shutil.rmtree(self.symbol_dir) |
|
143 subprocess.call = self._subprocess_call |
|
144 subprocess.Popen = self._subprocess_popen |
|
145 |
|
146 def mock_rmtree(self, path): |
|
147 pass |
|
148 |
|
149 def mock_call(self, args, **kwargs): |
|
150 if args[0].endswith("dsymutil"): |
|
151 filename = args[-1] |
|
152 os.makedirs(filename + ".dSYM") |
|
153 return 0 |
|
154 |
|
155 def next_mock_stdout(self): |
|
156 if not self.stdouts: |
|
157 yield iter([]) |
|
158 for s in self.stdouts: |
|
159 yield iter(s) |
|
160 |
|
161 def test_copy_debug_universal(self): |
|
162 """ |
|
163 Test that dumping symbols for multiple architectures only copies debug symbols once |
|
164 per file. |
|
165 """ |
|
166 copied = [] |
|
167 def mock_copy_debug(filename, debug_file, guid): |
|
168 copied.append(filename[len(self.symbol_dir):] if filename.startswith(self.symbol_dir) else filename) |
|
169 self.add_test_files(add_extension(["foo"])) |
|
170 self.stdouts.append(mock_dump_syms("X" * 33, add_extension(["foo"])[0])) |
|
171 self.stdouts.append(mock_dump_syms("Y" * 33, add_extension(["foo"])[0])) |
|
172 d = symbolstore.GetPlatformSpecificDumper(dump_syms="dump_syms", |
|
173 symbol_path=self.symbol_dir, |
|
174 copy_debug=True, |
|
175 archs="abc xyz") |
|
176 d.CopyDebug = mock_copy_debug |
|
177 d.Process(self.test_dir) |
|
178 d.Finish(stop_pool=False) |
|
179 self.assertEqual(1, len(copied)) |
|
180 |
|
181 class TestGetVCSFilename(HelperMixin, unittest.TestCase): |
|
182 def setUp(self): |
|
183 HelperMixin.setUp(self) |
|
184 |
|
185 def tearDown(self): |
|
186 HelperMixin.tearDown(self) |
|
187 |
|
188 @patch("subprocess.Popen") |
|
189 def testVCSFilenameHg(self, mock_Popen): |
|
190 # mock calls to `hg parent` and `hg showconfig paths.default` |
|
191 mock_communicate = mock_Popen.return_value.communicate |
|
192 mock_communicate.side_effect = [("abcd1234", ""), |
|
193 ("http://example.com/repo", "")] |
|
194 os.mkdir(os.path.join(self.test_dir, ".hg")) |
|
195 filename = os.path.join(self.test_dir, "foo.c") |
|
196 self.assertEqual("hg:example.com/repo:foo.c:abcd1234", |
|
197 symbolstore.GetVCSFilename(filename, [self.test_dir])[0]) |
|
198 |
|
199 @patch("subprocess.Popen") |
|
200 def testVCSFilenameHgMultiple(self, mock_Popen): |
|
201 # mock calls to `hg parent` and `hg showconfig paths.default` |
|
202 mock_communicate = mock_Popen.return_value.communicate |
|
203 mock_communicate.side_effect = [("abcd1234", ""), |
|
204 ("http://example.com/repo", ""), |
|
205 ("0987ffff", ""), |
|
206 ("http://example.com/other", "")] |
|
207 srcdir1 = os.path.join(self.test_dir, "one") |
|
208 srcdir2 = os.path.join(self.test_dir, "two") |
|
209 os.makedirs(os.path.join(srcdir1, ".hg")) |
|
210 os.makedirs(os.path.join(srcdir2, ".hg")) |
|
211 filename1 = os.path.join(srcdir1, "foo.c") |
|
212 filename2 = os.path.join(srcdir2, "bar.c") |
|
213 self.assertEqual("hg:example.com/repo:foo.c:abcd1234", |
|
214 symbolstore.GetVCSFilename(filename1, [srcdir1, srcdir2])[0]) |
|
215 self.assertEqual("hg:example.com/other:bar.c:0987ffff", |
|
216 symbolstore.GetVCSFilename(filename2, [srcdir1, srcdir2])[0]) |
|
217 |
|
218 class TestRepoManifest(HelperMixin, unittest.TestCase): |
|
219 def testRepoManifest(self): |
|
220 manifest = os.path.join(self.test_dir, "sources.xml") |
|
221 open(manifest, "w").write("""<?xml version="1.0" encoding="UTF-8"?> |
|
222 <manifest> |
|
223 <remote fetch="http://example.com/foo/" name="foo"/> |
|
224 <remote fetch="git://example.com/bar/" name="bar"/> |
|
225 <default remote="bar"/> |
|
226 <project name="projects/one" revision="abcd1234"/> |
|
227 <project name="projects/two" path="projects/another" revision="ffffffff" remote="foo"/> |
|
228 <project name="something_else" revision="00000000" remote="bar"/> |
|
229 </manifest> |
|
230 """) |
|
231 # Use a source file from each of the three projects |
|
232 file1 = os.path.join(self.test_dir, "projects", "one", "src1.c") |
|
233 file2 = os.path.join(self.test_dir, "projects", "another", "src2.c") |
|
234 file3 = os.path.join(self.test_dir, "something_else", "src3.c") |
|
235 d = symbolstore.Dumper("dump_syms", "symbol_path", |
|
236 repo_manifest=manifest) |
|
237 self.assertEqual("git:example.com/bar/projects/one:src1.c:abcd1234", |
|
238 symbolstore.GetVCSFilename(file1, d.srcdirs)[0]) |
|
239 self.assertEqual("git:example.com/foo/projects/two:src2.c:ffffffff", |
|
240 symbolstore.GetVCSFilename(file2, d.srcdirs)[0]) |
|
241 self.assertEqual("git:example.com/bar/something_else:src3.c:00000000", |
|
242 symbolstore.GetVCSFilename(file3, d.srcdirs)[0]) |
|
243 |
|
244 if platform.system() in ("Windows", "Microsoft"): |
|
245 class TestSourceServer(HelperMixin, unittest.TestCase): |
|
246 @patch("subprocess.call") |
|
247 @patch("subprocess.Popen") |
|
248 def test_HGSERVER(self, mock_Popen, mock_call): |
|
249 """ |
|
250 Test that HGSERVER gets set correctly in the source server index. |
|
251 """ |
|
252 symbolpath = os.path.join(self.test_dir, "symbols") |
|
253 os.makedirs(symbolpath) |
|
254 srcdir = os.path.join(self.test_dir, "srcdir") |
|
255 os.makedirs(os.path.join(srcdir, ".hg")) |
|
256 sourcefile = os.path.join(srcdir, "foo.c") |
|
257 test_files = add_extension(["foo"]) |
|
258 self.add_test_files(test_files) |
|
259 # srcsrv needs PDBSTR_PATH set |
|
260 os.environ["PDBSTR_PATH"] = "pdbstr" |
|
261 # mock calls to `dump_syms`, `hg parent` and |
|
262 # `hg showconfig paths.default` |
|
263 mock_Popen.return_value.stdout = iter([ |
|
264 "MODULE os x86 %s %s" % ("X" * 33, test_files[0]), |
|
265 "FILE 0 %s" % sourcefile, |
|
266 "PUBLIC xyz 123" |
|
267 ]) |
|
268 mock_communicate = mock_Popen.return_value.communicate |
|
269 mock_communicate.side_effect = [("abcd1234", ""), |
|
270 ("http://example.com/repo", ""), |
|
271 ] |
|
272 # And mock the call to pdbstr to capture the srcsrv stream data. |
|
273 global srcsrv_stream |
|
274 srcsrv_stream = None |
|
275 def mock_pdbstr(args, cwd="", **kwargs): |
|
276 for arg in args: |
|
277 if arg.startswith("-i:"): |
|
278 global srcsrv_stream |
|
279 srcsrv_stream = open(os.path.join(cwd, arg[3:]), 'r').read() |
|
280 return 0 |
|
281 mock_call.side_effect = mock_pdbstr |
|
282 d = symbolstore.GetPlatformSpecificDumper(dump_syms="dump_syms", |
|
283 symbol_path=symbolpath, |
|
284 srcdirs=[srcdir], |
|
285 vcsinfo=True, |
|
286 srcsrv=True, |
|
287 copy_debug=True) |
|
288 # stub out CopyDebug |
|
289 d.CopyDebug = lambda *args: True |
|
290 d.Process(self.test_dir) |
|
291 d.Finish(stop_pool=False) |
|
292 self.assertNotEqual(srcsrv_stream, None) |
|
293 hgserver = [x.rstrip() for x in srcsrv_stream.splitlines() if x.startswith("HGSERVER=")] |
|
294 self.assertEqual(len(hgserver), 1) |
|
295 self.assertEqual(hgserver[0].split("=")[1], "http://example.com/repo") |
|
296 |
|
297 if __name__ == '__main__': |
|
298 # use the multiprocessing.dummy module to use threading wrappers so |
|
299 # that our mocking/module-patching works |
|
300 symbolstore.Dumper.GlobalInit(module=multiprocessing.dummy) |
|
301 |
|
302 unittest.main() |
|
303 |
|
304 symbolstore.Dumper.pool.close() |
|
305 symbolstore.Dumper.pool.join() |