1import os
2import copy
3import pickle
4import platform
5import subprocess
6import sys
7import unittest
8from unittest import mock
9
10from test import support
11from test.support import os_helper
12
13FEDORA_OS_RELEASE = """\
14NAME=Fedora
15VERSION="32 (Thirty Two)"
16ID=fedora
17VERSION_ID=32
18VERSION_CODENAME=""
19PLATFORM_ID="platform:f32"
20PRETTY_NAME="Fedora 32 (Thirty Two)"
21ANSI_COLOR="0;34"
22LOGO=fedora-logo-icon
23CPE_NAME="cpe:/o:fedoraproject:fedora:32"
24HOME_URL="https://fedoraproject.org/"
25DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora/f32/system-administrators-guide/"
26SUPPORT_URL="https://fedoraproject.org/wiki/Communicating_and_getting_help"
27BUG_REPORT_URL="https://bugzilla.redhat.com/"
28REDHAT_BUGZILLA_PRODUCT="Fedora"
29REDHAT_BUGZILLA_PRODUCT_VERSION=32
30REDHAT_SUPPORT_PRODUCT="Fedora"
31REDHAT_SUPPORT_PRODUCT_VERSION=32
32PRIVACY_POLICY_URL="https://fedoraproject.org/wiki/Legal:PrivacyPolicy"
33"""
34
35UBUNTU_OS_RELEASE = """\
36NAME="Ubuntu"
37VERSION="20.04.1 LTS (Focal Fossa)"
38ID=ubuntu
39ID_LIKE=debian
40PRETTY_NAME="Ubuntu 20.04.1 LTS"
41VERSION_ID="20.04"
42HOME_URL="https://www.ubuntu.com/"
43SUPPORT_URL="https://help.ubuntu.com/"
44BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
45PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
46VERSION_CODENAME=focal
47UBUNTU_CODENAME=focal
48"""
49
50TEST_OS_RELEASE = r"""
51# test data
52ID_LIKE="egg spam viking"
53EMPTY=
54# comments and empty lines are ignored
55
56SINGLE_QUOTE='single'
57EMPTY_SINGLE=''
58DOUBLE_QUOTE="double"
59EMPTY_DOUBLE=""
60QUOTES="double\'s"
61SPECIALS="\$\`\\\'\""
62# invalid lines
63=invalid
64=
65INVALID
66IN-VALID=value
67IN VALID=value
68"""
69
70
71class PlatformTest(unittest.TestCase):
72    def clear_caches(self):
73        platform._platform_cache.clear()
74        platform._sys_version_cache.clear()
75        platform._uname_cache = None
76        platform._os_release_cache = None
77
78    def test_architecture(self):
79        res = platform.architecture()
80
81    @os_helper.skip_unless_symlink
82    def test_architecture_via_symlink(self): # issue3762
83        with support.PythonSymlink() as py:
84            cmd = "-c", "import platform; print(platform.architecture())"
85            self.assertEqual(py.call_real(*cmd), py.call_link(*cmd))
86
87    def test_platform(self):
88        for aliased in (False, True):
89            for terse in (False, True):
90                res = platform.platform(aliased, terse)
91
92    def test_system(self):
93        res = platform.system()
94
95    def test_node(self):
96        res = platform.node()
97
98    def test_release(self):
99        res = platform.release()
100
101    def test_version(self):
102        res = platform.version()
103
104    def test_machine(self):
105        res = platform.machine()
106
107    def test_processor(self):
108        res = platform.processor()
109
110    def setUp(self):
111        self.save_version = sys.version
112        self.save_git = sys._git
113        self.save_platform = sys.platform
114
115    def tearDown(self):
116        sys.version = self.save_version
117        sys._git = self.save_git
118        sys.platform = self.save_platform
119
120    def test_sys_version(self):
121        # Old test.
122        for input, output in (
123            ('2.4.3 (#1, Jun 21 2006, 13:54:21) \n[GCC 3.3.4 (pre 3.3.5 20040809)]',
124             ('CPython', '2.4.3', '', '', '1', 'Jun 21 2006 13:54:21', 'GCC 3.3.4 (pre 3.3.5 20040809)')),
125            ('IronPython 1.0.60816 on .NET 2.0.50727.42',
126             ('IronPython', '1.0.60816', '', '', '', '', '.NET 2.0.50727.42')),
127            ('IronPython 1.0 (1.0.61005.1977) on .NET 2.0.50727.42',
128             ('IronPython', '1.0.0', '', '', '', '', '.NET 2.0.50727.42')),
129            ('2.4.3 (truncation, date, t) \n[GCC]',
130             ('CPython', '2.4.3', '', '', 'truncation', 'date t', 'GCC')),
131            ('2.4.3 (truncation, date, ) \n[GCC]',
132             ('CPython', '2.4.3', '', '', 'truncation', 'date', 'GCC')),
133            ('2.4.3 (truncation, date,) \n[GCC]',
134             ('CPython', '2.4.3', '', '', 'truncation', 'date', 'GCC')),
135            ('2.4.3 (truncation, date) \n[GCC]',
136             ('CPython', '2.4.3', '', '', 'truncation', 'date', 'GCC')),
137            ('2.4.3 (truncation, d) \n[GCC]',
138             ('CPython', '2.4.3', '', '', 'truncation', 'd', 'GCC')),
139            ('2.4.3 (truncation, ) \n[GCC]',
140             ('CPython', '2.4.3', '', '', 'truncation', '', 'GCC')),
141            ('2.4.3 (truncation,) \n[GCC]',
142             ('CPython', '2.4.3', '', '', 'truncation', '', 'GCC')),
143            ('2.4.3 (truncation) \n[GCC]',
144             ('CPython', '2.4.3', '', '', 'truncation', '', 'GCC')),
145            ):
146            # branch and revision are not "parsed", but fetched
147            # from sys._git.  Ignore them
148            (name, version, branch, revision, buildno, builddate, compiler) \
149                   = platform._sys_version(input)
150            self.assertEqual(
151                (name, version, '', '', buildno, builddate, compiler), output)
152
153        # Tests for python_implementation(), python_version(), python_branch(),
154        # python_revision(), python_build(), and python_compiler().
155        sys_versions = {
156            ("2.6.1 (r261:67515, Dec  6 2008, 15:26:00) \n[GCC 4.0.1 (Apple Computer, Inc. build 5370)]",
157             ('CPython', 'tags/r261', '67515'), self.save_platform)
158            :
159                ("CPython", "2.6.1", "tags/r261", "67515",
160                 ('r261:67515', 'Dec  6 2008 15:26:00'),
161                 'GCC 4.0.1 (Apple Computer, Inc. build 5370)'),
162
163            ("IronPython 2.0 (2.0.0.0) on .NET 2.0.50727.3053", None, "cli")
164            :
165                ("IronPython", "2.0.0", "", "", ("", ""),
166                 ".NET 2.0.50727.3053"),
167
168            ("2.6.1 (IronPython 2.6.1 (2.6.10920.0) on .NET 2.0.50727.1433)", None, "cli")
169            :
170                ("IronPython", "2.6.1", "", "", ("", ""),
171                 ".NET 2.0.50727.1433"),
172
173            ("2.7.4 (IronPython 2.7.4 (2.7.0.40) on Mono 4.0.30319.1 (32-bit))", None, "cli")
174            :
175                ("IronPython", "2.7.4", "", "", ("", ""),
176                 "Mono 4.0.30319.1 (32-bit)"),
177
178            ("2.5 (trunk:6107, Mar 26 2009, 13:02:18) \n[Java HotSpot(TM) Client VM (\"Apple Computer, Inc.\")]",
179            ('Jython', 'trunk', '6107'), "java1.5.0_16")
180            :
181                ("Jython", "2.5.0", "trunk", "6107",
182                 ('trunk:6107', 'Mar 26 2009'), "java1.5.0_16"),
183
184            ("2.5.2 (63378, Mar 26 2009, 18:03:29)\n[PyPy 1.0.0]",
185             ('PyPy', 'trunk', '63378'), self.save_platform)
186            :
187                ("PyPy", "2.5.2", "trunk", "63378", ('63378', 'Mar 26 2009'),
188                 "")
189            }
190        for (version_tag, scm, sys_platform), info in \
191                sys_versions.items():
192            sys.version = version_tag
193            if scm is None:
194                if hasattr(sys, "_git"):
195                    del sys._git
196            else:
197                sys._git = scm
198            if sys_platform is not None:
199                sys.platform = sys_platform
200            self.assertEqual(platform.python_implementation(), info[0])
201            self.assertEqual(platform.python_version(), info[1])
202            self.assertEqual(platform.python_branch(), info[2])
203            self.assertEqual(platform.python_revision(), info[3])
204            self.assertEqual(platform.python_build(), info[4])
205            self.assertEqual(platform.python_compiler(), info[5])
206
207    def test_system_alias(self):
208        res = platform.system_alias(
209            platform.system(),
210            platform.release(),
211            platform.version(),
212        )
213
214    def test_uname(self):
215        res = platform.uname()
216        self.assertTrue(any(res))
217        self.assertEqual(res[0], res.system)
218        self.assertEqual(res[-6], res.system)
219        self.assertEqual(res[1], res.node)
220        self.assertEqual(res[-5], res.node)
221        self.assertEqual(res[2], res.release)
222        self.assertEqual(res[-4], res.release)
223        self.assertEqual(res[3], res.version)
224        self.assertEqual(res[-3], res.version)
225        self.assertEqual(res[4], res.machine)
226        self.assertEqual(res[-2], res.machine)
227        self.assertEqual(res[5], res.processor)
228        self.assertEqual(res[-1], res.processor)
229        self.assertEqual(len(res), 6)
230
231    def test_uname_cast_to_tuple(self):
232        res = platform.uname()
233        expected = (
234            res.system, res.node, res.release, res.version, res.machine,
235            res.processor,
236        )
237        self.assertEqual(tuple(res), expected)
238
239    def test_uname_replace(self):
240        res = platform.uname()
241        new = res._replace(
242            system='system', node='node', release='release',
243            version='version', machine='machine')
244        self.assertEqual(new.system, 'system')
245        self.assertEqual(new.node, 'node')
246        self.assertEqual(new.release, 'release')
247        self.assertEqual(new.version, 'version')
248        self.assertEqual(new.machine, 'machine')
249        # processor cannot be replaced
250        self.assertEqual(new.processor, res.processor)
251
252    def test_uname_copy(self):
253        uname = platform.uname()
254        self.assertEqual(copy.copy(uname), uname)
255        self.assertEqual(copy.deepcopy(uname), uname)
256
257    def test_uname_pickle(self):
258        orig = platform.uname()
259        for proto in range(pickle.HIGHEST_PROTOCOL + 1):
260            with self.subTest(protocol=proto):
261                pickled = pickle.dumps(orig, proto)
262                restored = pickle.loads(pickled)
263                self.assertEqual(restored, orig)
264
265    def test_uname_slices(self):
266        res = platform.uname()
267        expected = tuple(res)
268        self.assertEqual(res[:], expected)
269        self.assertEqual(res[:5], expected[:5])
270
271    @unittest.skipIf(sys.platform in ['win32', 'OpenVMS'], "uname -p not used")
272    def test_uname_processor(self):
273        """
274        On some systems, the processor must match the output
275        of 'uname -p'. See Issue 35967 for rationale.
276        """
277        try:
278            proc_res = subprocess.check_output(['uname', '-p'], text=True).strip()
279            expect = platform._unknown_as_blank(proc_res)
280        except (OSError, subprocess.CalledProcessError):
281            expect = ''
282        self.assertEqual(platform.uname().processor, expect)
283
284    @unittest.skipUnless(sys.platform.startswith('win'), "windows only test")
285    def test_uname_win32_ARCHITEW6432(self):
286        # Issue 7860: make sure we get architecture from the correct variable
287        # on 64 bit Windows: if PROCESSOR_ARCHITEW6432 exists we should be
288        # using it, per
289        # http://blogs.msdn.com/david.wang/archive/2006/03/26/HOWTO-Detect-Process-Bitness.aspx
290        try:
291            with os_helper.EnvironmentVarGuard() as environ:
292                if 'PROCESSOR_ARCHITEW6432' in environ:
293                    del environ['PROCESSOR_ARCHITEW6432']
294                environ['PROCESSOR_ARCHITECTURE'] = 'foo'
295                platform._uname_cache = None
296                system, node, release, version, machine, processor = platform.uname()
297                self.assertEqual(machine, 'foo')
298                environ['PROCESSOR_ARCHITEW6432'] = 'bar'
299                platform._uname_cache = None
300                system, node, release, version, machine, processor = platform.uname()
301                self.assertEqual(machine, 'bar')
302        finally:
303            platform._uname_cache = None
304
305    def test_java_ver(self):
306        res = platform.java_ver()
307        if sys.platform == 'java':
308            self.assertTrue(all(res))
309
310    def test_win32_ver(self):
311        res = platform.win32_ver()
312
313    def test_mac_ver(self):
314        res = platform.mac_ver()
315
316        if platform.uname().system == 'Darwin':
317            # We are on a macOS system, check that the right version
318            # information is returned
319            output = subprocess.check_output(['sw_vers'], text=True)
320            for line in output.splitlines():
321                if line.startswith('ProductVersion:'):
322                    real_ver = line.strip().split()[-1]
323                    break
324            else:
325                self.fail(f"failed to parse sw_vers output: {output!r}")
326
327            result_list = res[0].split('.')
328            expect_list = real_ver.split('.')
329            len_diff = len(result_list) - len(expect_list)
330            # On Snow Leopard, sw_vers reports 10.6.0 as 10.6
331            if len_diff > 0:
332                expect_list.extend(['0'] * len_diff)
333            # For compatibility with older binaries, macOS 11.x may report
334            # itself as '10.16' rather than '11.x.y'.
335            if result_list != ['10', '16']:
336                self.assertEqual(result_list, expect_list)
337
338            # res[1] claims to contain
339            # (version, dev_stage, non_release_version)
340            # That information is no longer available
341            self.assertEqual(res[1], ('', '', ''))
342
343            if sys.byteorder == 'little':
344                self.assertIn(res[2], ('i386', 'x86_64', 'arm64'))
345            else:
346                self.assertEqual(res[2], 'PowerPC')
347
348
349    @unittest.skipUnless(sys.platform == 'darwin', "OSX only test")
350    def test_mac_ver_with_fork(self):
351        # Issue7895: platform.mac_ver() crashes when using fork without exec
352        #
353        # This test checks that the fix for that issue works.
354        #
355        pid = os.fork()
356        if pid == 0:
357            # child
358            info = platform.mac_ver()
359            os._exit(0)
360
361        else:
362            # parent
363            support.wait_process(pid, exitcode=0)
364
365    def test_libc_ver(self):
366        # check that libc_ver(executable) doesn't raise an exception
367        if os.path.isdir(sys.executable) and \
368           os.path.exists(sys.executable+'.exe'):
369            # Cygwin horror
370            executable = sys.executable + '.exe'
371        elif sys.platform == "win32" and not os.path.exists(sys.executable):
372            # App symlink appears to not exist, but we want the
373            # real executable here anyway
374            import _winapi
375            executable = _winapi.GetModuleFileName(0)
376        else:
377            executable = sys.executable
378        platform.libc_ver(executable)
379
380        filename = os_helper.TESTFN
381        self.addCleanup(os_helper.unlink, filename)
382
383        with mock.patch('os.confstr', create=True, return_value='mock 1.0'):
384            # test os.confstr() code path
385            self.assertEqual(platform.libc_ver(), ('mock', '1.0'))
386
387            # test the different regular expressions
388            for data, expected in (
389                (b'__libc_init', ('libc', '')),
390                (b'GLIBC_2.9', ('glibc', '2.9')),
391                (b'libc.so.1.2.5', ('libc', '1.2.5')),
392                (b'libc_pthread.so.1.2.5', ('libc', '1.2.5_pthread')),
393                (b'', ('', '')),
394            ):
395                with open(filename, 'wb') as fp:
396                    fp.write(b'[xxx%sxxx]' % data)
397                    fp.flush()
398
399                # os.confstr() must not be used if executable is set
400                self.assertEqual(platform.libc_ver(executable=filename),
401                                 expected)
402
403        # binary containing multiple versions: get the most recent,
404        # make sure that 1.9 is seen as older than 1.23.4
405        chunksize = 16384
406        with open(filename, 'wb') as f:
407            # test match at chunk boundary
408            f.write(b'x'*(chunksize - 10))
409            f.write(b'GLIBC_1.23.4\0GLIBC_1.9\0GLIBC_1.21\0')
410        self.assertEqual(platform.libc_ver(filename, chunksize=chunksize),
411                         ('glibc', '1.23.4'))
412
413    @support.cpython_only
414    def test__comparable_version(self):
415        from platform import _comparable_version as V
416        self.assertEqual(V('1.2.3'), V('1.2.3'))
417        self.assertLess(V('1.2.3'), V('1.2.10'))
418        self.assertEqual(V('1.2.3.4'), V('1_2-3+4'))
419        self.assertLess(V('1.2spam'), V('1.2dev'))
420        self.assertLess(V('1.2dev'), V('1.2alpha'))
421        self.assertLess(V('1.2dev'), V('1.2a'))
422        self.assertLess(V('1.2alpha'), V('1.2beta'))
423        self.assertLess(V('1.2a'), V('1.2b'))
424        self.assertLess(V('1.2beta'), V('1.2c'))
425        self.assertLess(V('1.2b'), V('1.2c'))
426        self.assertLess(V('1.2c'), V('1.2RC'))
427        self.assertLess(V('1.2c'), V('1.2rc'))
428        self.assertLess(V('1.2RC'), V('1.2.0'))
429        self.assertLess(V('1.2rc'), V('1.2.0'))
430        self.assertLess(V('1.2.0'), V('1.2pl'))
431        self.assertLess(V('1.2.0'), V('1.2p'))
432
433        self.assertLess(V('1.5.1'), V('1.5.2b2'))
434        self.assertLess(V('3.10a'), V('161'))
435        self.assertEqual(V('8.02'), V('8.02'))
436        self.assertLess(V('3.4j'), V('1996.07.12'))
437        self.assertLess(V('3.1.1.6'), V('3.2.pl0'))
438        self.assertLess(V('2g6'), V('11g'))
439        self.assertLess(V('0.9'), V('2.2'))
440        self.assertLess(V('1.2'), V('1.2.1'))
441        self.assertLess(V('1.1'), V('1.2.2'))
442        self.assertLess(V('1.1'), V('1.2'))
443        self.assertLess(V('1.2.1'), V('1.2.2'))
444        self.assertLess(V('1.2'), V('1.2.2'))
445        self.assertLess(V('0.4'), V('0.4.0'))
446        self.assertLess(V('1.13++'), V('5.5.kw'))
447        self.assertLess(V('0.960923'), V('2.2beta29'))
448
449
450    def test_macos(self):
451        self.addCleanup(self.clear_caches)
452
453        uname = ('Darwin', 'hostname', '17.7.0',
454                 ('Darwin Kernel Version 17.7.0: '
455                  'Thu Jun 21 22:53:14 PDT 2018; '
456                  'root:xnu-4570.71.2~1/RELEASE_X86_64'),
457                 'x86_64', 'i386')
458        arch = ('64bit', '')
459        with mock.patch.object(platform, 'uname', return_value=uname), \
460             mock.patch.object(platform, 'architecture', return_value=arch):
461            for mac_ver, expected_terse, expected in [
462                # darwin: mac_ver() returns empty strings
463                (('', '', ''),
464                 'Darwin-17.7.0',
465                 'Darwin-17.7.0-x86_64-i386-64bit'),
466                # macOS: mac_ver() returns macOS version
467                (('10.13.6', ('', '', ''), 'x86_64'),
468                 'macOS-10.13.6',
469                 'macOS-10.13.6-x86_64-i386-64bit'),
470            ]:
471                with mock.patch.object(platform, 'mac_ver',
472                                       return_value=mac_ver):
473                    self.clear_caches()
474                    self.assertEqual(platform.platform(terse=1), expected_terse)
475                    self.assertEqual(platform.platform(), expected)
476
477    def test_freedesktop_os_release(self):
478        self.addCleanup(self.clear_caches)
479        self.clear_caches()
480
481        if any(os.path.isfile(fn) for fn in platform._os_release_candidates):
482            info = platform.freedesktop_os_release()
483            self.assertIn("NAME", info)
484            self.assertIn("ID", info)
485
486            info["CPYTHON_TEST"] = "test"
487            self.assertNotIn(
488                "CPYTHON_TEST",
489                platform.freedesktop_os_release()
490            )
491        else:
492            with self.assertRaises(OSError):
493                platform.freedesktop_os_release()
494
495    def test_parse_os_release(self):
496        info = platform._parse_os_release(FEDORA_OS_RELEASE.splitlines())
497        self.assertEqual(info["NAME"], "Fedora")
498        self.assertEqual(info["ID"], "fedora")
499        self.assertNotIn("ID_LIKE", info)
500        self.assertEqual(info["VERSION_CODENAME"], "")
501
502        info = platform._parse_os_release(UBUNTU_OS_RELEASE.splitlines())
503        self.assertEqual(info["NAME"], "Ubuntu")
504        self.assertEqual(info["ID"], "ubuntu")
505        self.assertEqual(info["ID_LIKE"], "debian")
506        self.assertEqual(info["VERSION_CODENAME"], "focal")
507
508        info = platform._parse_os_release(TEST_OS_RELEASE.splitlines())
509        expected = {
510            "ID": "linux",
511            "NAME": "Linux",
512            "PRETTY_NAME": "Linux",
513            "ID_LIKE": "egg spam viking",
514            "EMPTY": "",
515            "DOUBLE_QUOTE": "double",
516            "EMPTY_DOUBLE": "",
517            "SINGLE_QUOTE": "single",
518            "EMPTY_SINGLE": "",
519            "QUOTES": "double's",
520            "SPECIALS": "$`\\'\"",
521        }
522        self.assertEqual(info, expected)
523        self.assertEqual(len(info["SPECIALS"]), 5)
524
525
526if __name__ == '__main__':
527    unittest.main()
528