diff --git a/Lib/test/support/os_helper.py b/Lib/test/support/os_helper.py index a6ec1cfa25ebe0..ff2fc338cbf239 100644 --- a/Lib/test/support/os_helper.py +++ b/Lib/test/support/os_helper.py @@ -237,6 +237,42 @@ def skip_unless_xattr(test): return test if ok else unittest.skip(msg)(test) +_can_chmod = None + +def can_chmod(): + global _can_chmod + if _can_chmod is not None: + return _can_chmod + if not hasattr(os, "chown"): + _can_chmod = False + return _can_chmod + try: + with open(TESTFN, "wb") as f: + try: + os.chmod(TESTFN, 0o777) + mode1 = os.stat(TESTFN).st_mode + os.chmod(TESTFN, 0o666) + mode2 = os.stat(TESTFN).st_mode + except OSError as e: + can = False + else: + can = stat.S_IMODE(mode1) != stat.S_IMODE(mode2) + finally: + os.unlink(TESTFN) + _can_chmod = can + return can + + +def skip_unless_working_chmod(test): + """Skip tests that require working os.chmod() + + WASI SDK 15.0 cannot change file mode bits. + """ + ok = can_chmod() + msg = "requires working os.chmod()" + return test if ok else unittest.skip(msg)(test) + + def unlink(filename): try: _unlink(filename) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 273db45c00f7ac..299eb30b4332bf 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -45,6 +45,7 @@ def setUp(self): env['COLUMNS'] = '80' +@os_helper.skip_unless_working_chmod class TempDirMixin(object): def setUp(self): diff --git a/Lib/test/test_dbm_dumb.py b/Lib/test/test_dbm_dumb.py index 73cff638f1e1a4..a481175b3bfdbd 100644 --- a/Lib/test/test_dbm_dumb.py +++ b/Lib/test/test_dbm_dumb.py @@ -42,6 +42,7 @@ def test_dumbdbm_creation(self): self.read_helper(f) @unittest.skipUnless(hasattr(os, 'umask'), 'test needs os.umask()') + @os_helper.skip_unless_working_chmod def test_dumbdbm_creation_mode(self): try: old_umask = os.umask(0o002) @@ -265,6 +266,7 @@ def test_invalid_flag(self): "'r', 'w', 'c', or 'n'"): dumbdbm.open(_fname, flag) + @os_helper.skip_unless_working_chmod def test_readonly_files(self): with os_helper.temp_dir() as dir: fname = os.path.join(dir, 'db') diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 35c260bd634fc6..be2e91ddd9a9ab 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -557,6 +557,7 @@ def test_creation_mode(self): @unittest.skipUnless(os.name == 'posix', "test meaningful only on posix systems") + @os_helper.skip_unless_working_chmod def test_cached_mode_issue_2051(self): # permissions of .pyc should match those of .py, regardless of mask mode = 0o600 @@ -573,6 +574,7 @@ def test_cached_mode_issue_2051(self): @unittest.skipUnless(os.name == 'posix', "test meaningful only on posix systems") + @os_helper.skip_unless_working_chmod def test_cached_readonly(self): mode = 0o400 with temp_umask(0o022), _ready_to_import() as (name, path): @@ -886,6 +888,7 @@ def test_import_pyc_path(self): @unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0, "due to varying filesystem permission semantics (issue #11956)") @skip_if_dont_write_bytecode + @os_helper.skip_unless_working_chmod def test_unwritable_directory(self): # When the umask causes the new __pycache__ directory to be # unwritable, the import still succeeds but no .pyc file is written. diff --git a/Lib/test/test_netrc.py b/Lib/test/test_netrc.py index 05a23e5e8a3ce2..573d636de956d1 100644 --- a/Lib/test/test_netrc.py +++ b/Lib/test/test_netrc.py @@ -272,6 +272,7 @@ def test_comment_at_end_of_machine_line_pass_has_hash(self): @unittest.skipUnless(os.name == 'posix', 'POSIX only test') @unittest.skipIf(pwd is None, 'security check requires pwd module') + @os_helper.skip_unless_working_chmod def test_security(self): # This test is incomplete since we are normally not run as root and # therefore can't test the file ownership being wrong. diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index c0321dcdcc071a..29f69a8f475b20 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -1670,7 +1670,7 @@ def tearDown(self): os.removedirs(path) -@unittest.skipUnless(hasattr(os, 'chown'), "Test needs chown") +@os_helper.skip_unless_working_chmod class ChownFileTests(unittest.TestCase): @classmethod @@ -3784,7 +3784,6 @@ class Str(str): def test_oserror_filename(self): funcs = [ (self.filenames, os.chdir,), - (self.filenames, os.chmod, 0o777), (self.filenames, os.lstat,), (self.filenames, os.open, os.O_RDONLY), (self.filenames, os.rmdir,), @@ -3805,6 +3804,8 @@ def test_oserror_filename(self): (self.filenames, os.rename, "dst"), (self.filenames, os.replace, "dst"), )) + if os_helper.can_chmod(): + funcs.append((self.filenames, os.chmod, 0o777)) if hasattr(os, "chown"): funcs.append((self.filenames, os.chown, 0, 0)) if hasattr(os, "lchown"): diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index e2da115501abe6..3b88c0848a0448 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -1902,6 +1902,7 @@ def test_with(self): with p: pass + @os_helper.skip_unless_working_chmod def test_chmod(self): p = self.cls(BASE) / 'fileA' mode = p.stat().st_mode @@ -1916,6 +1917,7 @@ def test_chmod(self): # On Windows, os.chmod does not follow symlinks (issue #15411) @only_posix + @os_helper.skip_unless_working_chmod def test_chmod_follow_symlinks_true(self): p = self.cls(BASE) / 'linkA' q = p.resolve() @@ -1931,6 +1933,7 @@ def test_chmod_follow_symlinks_true(self): # XXX also need a test for lchmod. + @os_helper.skip_unless_working_chmod def test_stat(self): p = self.cls(BASE) / 'fileA' st = p.stat() diff --git a/Lib/test/test_posix.py b/Lib/test/test_posix.py index 28e5e90297e242..f45fae7edb8e4d 100644 --- a/Lib/test/test_posix.py +++ b/Lib/test/test_posix.py @@ -784,7 +784,7 @@ def check_stat(uid, gid): self.assertRaises(TypeError, chown_func, first_param, uid, t(gid)) check_stat(uid, gid) - @unittest.skipUnless(hasattr(posix, 'chown'), "test needs os.chown()") + @os_helper.skip_unless_working_chmod def test_chown(self): # raise an OSError if the file does not exist os.unlink(os_helper.TESTFN) @@ -794,6 +794,7 @@ def test_chown(self): os_helper.create_empty_file(os_helper.TESTFN) self._test_all_chown_common(posix.chown, os_helper.TESTFN, posix.stat) + @os_helper.skip_unless_working_chmod @unittest.skipUnless(hasattr(posix, 'fchown'), "test needs os.fchown()") def test_fchown(self): os.unlink(os_helper.TESTFN) @@ -807,6 +808,7 @@ def test_fchown(self): finally: test_file.close() + @os_helper.skip_unless_working_chmod @unittest.skipUnless(hasattr(posix, 'lchown'), "test needs os.lchown()") def test_lchown(self): os.unlink(os_helper.TESTFN) diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py index 97d3e9ea15bf33..c644f881e460fe 100644 --- a/Lib/test/test_posixpath.py +++ b/Lib/test/test_posixpath.py @@ -193,8 +193,7 @@ def test_ismount_non_existent(self): self.assertIs(posixpath.ismount('/\x00'), False) self.assertIs(posixpath.ismount(b'/\x00'), False) - @unittest.skipUnless(os_helper.can_symlink(), - "Test requires symlink support") + @os_helper.skip_unless_symlink def test_ismount_symlinks(self): # Symlinks are never mountpoints. try: diff --git a/Lib/test/test_py_compile.py b/Lib/test/test_py_compile.py index 794d6436b61ab0..f494aed42feae3 100644 --- a/Lib/test/test_py_compile.py +++ b/Lib/test/test_py_compile.py @@ -119,6 +119,7 @@ def test_relative_path(self): 'non-root user required') @unittest.skipIf(os.name == 'nt', 'cannot control directory permissions on Windows') + @os_helper.skip_unless_working_chmod def test_exceptions_propagate(self): # Make sure that exceptions raised thanks to issues with writing # bytecode. diff --git a/Lib/test/test_pydoc.py b/Lib/test/test_pydoc.py index ac181effe49bb6..b705b7aee8136d 100644 --- a/Lib/test/test_pydoc.py +++ b/Lib/test/test_pydoc.py @@ -933,6 +933,7 @@ def test_apropos_with_unreadable_dir(self): self.assertEqual(out.getvalue(), '') self.assertEqual(err.getvalue(), '') + @os_helper.skip_unless_working_chmod def test_apropos_empty_doc(self): pkgdir = os.path.join(TESTFN, 'walkpkg') os.mkdir(pkgdir) diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index c94390589af3ed..18421fca9c21b6 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -311,6 +311,7 @@ def onerror(*args): "This test can't be run on Cygwin (issue #1071513).") @unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0, "This test can't be run reliably as root (issue #1076467).") + @os_helper.skip_unless_working_chmod def test_on_error(self): self.errorState = 0 os.mkdir(TESTFN) diff --git a/Lib/test/test_stat.py b/Lib/test/test_stat.py index 193a0fc15d9bca..4ba37aed2dc9db 100644 --- a/Lib/test/test_stat.py +++ b/Lib/test/test_stat.py @@ -113,6 +113,7 @@ def assertS_IS(self, name, mode): else: self.assertFalse(func(mode)) + @os_helper.skip_unless_working_chmod def test_mode(self): with open(TESTFN, 'w'): pass @@ -151,6 +152,7 @@ def test_mode(self): self.assertEqual(self.statmod.S_IFMT(st_mode), self.statmod.S_IFREG) + @os_helper.skip_unless_working_chmod def test_directory(self): os.mkdir(TESTFN) os.chmod(TESTFN, 0o700) diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index a364043d3d9dd8..5c084688dc24d5 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -630,6 +630,7 @@ def test_extract_hardlink(self): data = f.read() self.assertEqual(sha256sum(data), sha256_regtype) + @os_helper.skip_unless_working_chmod def test_extractall(self): # Test if extractall() correctly restores directory permissions # and times (see issue1735). @@ -660,6 +661,7 @@ def format_mtime(mtime): tar.close() os_helper.rmtree(DIR) + @os_helper.skip_unless_working_chmod def test_extract_directory(self): dirtype = "ustar/dirtype" DIR = os.path.join(TEMPDIR, "extractdir") diff --git a/Lib/test/test_tempfile.py b/Lib/test/test_tempfile.py index f056e5ccb17f92..20f88d8c71e89a 100644 --- a/Lib/test/test_tempfile.py +++ b/Lib/test/test_tempfile.py @@ -450,6 +450,7 @@ def test_choose_directory(self): support.gc_collect() # For PyPy or other GCs. os.rmdir(dir) + @os_helper.skip_unless_working_chmod def test_file_mode(self): # _mkstemp_inner creates files with the proper mode @@ -787,6 +788,7 @@ def test_choose_directory(self): finally: os.rmdir(dir) + @os_helper.skip_unless_working_chmod def test_mode(self): # mkdtemp creates directories with the proper mode diff --git a/Lib/test/test_uu.py b/Lib/test/test_uu.py index 316a04af1cdaa7..0493aae4fc67be 100644 --- a/Lib/test/test_uu.py +++ b/Lib/test/test_uu.py @@ -74,6 +74,7 @@ def test_encode(self): with self.assertRaises(TypeError): uu.encode(inp, out, "t1", 0o644, True) + @os_helper.skip_unless_working_chmod def test_decode(self): for backtick in True, False: inp = io.BytesIO(encodedtextwrapped(0o666, "t1", backtick=backtick)) @@ -199,6 +200,8 @@ def test_encode(self): s = fout.read() self.assertEqual(s, encodedtextwrapped(0o644, self.tmpin)) + # decode() calls chmod() + @os_helper.skip_unless_working_chmod def test_decode(self): with open(self.tmpin, 'wb') as f: f.write(encodedtextwrapped(0o644, self.tmpout)) @@ -211,6 +214,7 @@ def test_decode(self): self.assertEqual(s, plaintext) # XXX is there an xp way to verify the mode? + @os_helper.skip_unless_working_chmod def test_decode_filename(self): with open(self.tmpin, 'wb') as f: f.write(encodedtextwrapped(0o644, self.tmpout)) @@ -221,6 +225,7 @@ def test_decode_filename(self): s = f.read() self.assertEqual(s, plaintext) + @os_helper.skip_unless_working_chmod def test_decodetwice(self): # Verify that decode() will refuse to overwrite an existing file with open(self.tmpin, 'wb') as f: @@ -231,6 +236,7 @@ def test_decodetwice(self): with open(self.tmpin, 'rb') as f: self.assertRaises(uu.Error, uu.decode, f) + @os_helper.skip_unless_working_chmod def test_decode_mode(self): # Verify that decode() will set the given mode for the out_file expected_mode = 0o444 diff --git a/Lib/test/test_zipapp.py b/Lib/test/test_zipapp.py index 69f2e55d563840..371e60118fff96 100644 --- a/Lib/test/test_zipapp.py +++ b/Lib/test/test_zipapp.py @@ -9,6 +9,7 @@ import zipapp import zipfile from test.support import requires_zlib +from test.support import os_helper from unittest.mock import patch @@ -301,6 +302,7 @@ def test_content_of_copied_archive(self): # (Unix only) tests that archives with shebang lines are made executable @unittest.skipIf(sys.platform == 'win32', 'Windows does not support an executable bit') + @os_helper.skip_unless_working_chmod def test_shebang_is_executable(self): # Test that an archive with a shebang line is made executable. source = self.tmpdir / 'source' diff --git a/Misc/NEWS.d/next/Tests/2022-06-05-10-16-45.gh-issue-90473.QMu7A8.rst b/Misc/NEWS.d/next/Tests/2022-06-05-10-16-45.gh-issue-90473.QMu7A8.rst new file mode 100644 index 00000000000000..6c76b7f4990e4f --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2022-06-05-10-16-45.gh-issue-90473.QMu7A8.rst @@ -0,0 +1,2 @@ +WASI does not have a ``chmod(2)`` syscall. :func:`os.chmod` is now a dummy +function on WASI. Skip all tests that depend on working :func:`os.chmod`. diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 40158894411bad..d7cac2b67ffdf8 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -3308,6 +3308,10 @@ os_chmod_impl(PyObject *module, path_t *path, int mode, int dir_fd, { #ifdef HAVE_CHMOD result = chmod(path->narrow, mode); +#elif defined(__wasi__) + // WASI SDK 15.0 does not support chmod. + // Ignore missing syscall for now. + result = 0; #else result = -1; errno = ENOSYS; diff --git a/Tools/wasm/config.site-wasm32-wasi b/Tools/wasm/config.site-wasm32-wasi index a6fcbed48fa811..f151b7bc5ab0c9 100644 --- a/Tools/wasm/config.site-wasm32-wasi +++ b/Tools/wasm/config.site-wasm32-wasi @@ -35,6 +35,11 @@ ac_cv_func_fdopendir=no # WASIX stubs we don't want to use. ac_cv_func_kill=no +# WASI SDK 15.0 does not have chmod. +# Ignore WASIX stubs for now. +ac_cv_func_chmod=no +ac_cv_func_fchmod=no + # WASI sockets are limited to operations on given socket fd and inet sockets. # Disable AF_UNIX and AF_PACKET support, see socketmodule.h. ac_cv_header_sys_un_h=no