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