Skip to content

Commit f1982ee

Browse files
committed
Implement statvfs/fstatvfs in Posix Java backend
1 parent a062c7c commit f1982ee

File tree

6 files changed

+148
-8
lines changed

6 files changed

+148
-8
lines changed

graalpython/com.oracle.graal.python.test/src/tests/test_posix.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,6 @@ def test_fstat(self):
380380
with open(TEST_FULL_PATH2, 0) as fd: # follows symlink
381381
self.assertEqual(inode, os.fstat(fd).st_ino)
382382

383-
@unittest.skipIf(__graalpython__.posix_module_backend() == 'java', 'statvfs emulation is not supported')
384383
def test_statvfs(self):
385384
res = os.statvfs(TEST_FULL_PATH1)
386385
with open(TEST_FULL_PATH1, 0) as fd:
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
2+
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
3+
#
4+
# The Universal Permissive License (UPL), Version 1.0
5+
#
6+
# Subject to the condition set forth below, permission is hereby granted to any
7+
# person obtaining a copy of this software, associated documentation and/or
8+
# data (collectively the "Software"), free of charge and under any and all
9+
# copyright rights in the Software, and any and all patent rights owned or
10+
# freely licensable by each licensor hereunder covering either (i) the
11+
# unmodified Software as contributed to or provided by such licensor, or (ii)
12+
# the Larger Works (as defined below), to deal in both
13+
#
14+
# (a) the Software, and
15+
#
16+
# (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
17+
# one is included with the Software each a "Larger Work" to which the Software
18+
# is contributed by such licensors),
19+
#
20+
# without restriction, including without limitation the rights to copy, create
21+
# derivative works of, display, perform, and distribute the Software and make,
22+
# use, sell, offer for sale, import, export, have made, and have sold the
23+
# Software and the Larger Work(s), and to sublicense the foregoing rights on
24+
# either these or other terms.
25+
#
26+
# This license is subject to the following condition:
27+
#
28+
# The above copyright notice and either this complete permission notice or at a
29+
# minimum a reference to the UPL must be included in all copies or substantial
30+
# portions of the Software.
31+
#
32+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
33+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
34+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
35+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
36+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
37+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
38+
# SOFTWARE.
39+
40+
import unittest
41+
import shutil
42+
import io
43+
import os
44+
import pathlib
45+
import re
46+
47+
48+
class TestShUtil(unittest.TestCase):
49+
50+
def test_disk_usage_returns_a_tuple(self):
51+
usage = shutil.disk_usage(__file__)
52+
self.assertIsInstance(usage, tuple)
53+
self.assertGreater(usage.total, 0)
54+
self.assertGreater(usage.used, 0)
55+
self.assertGreater(usage.free, 0)
56+
self.assertGreater(usage.total, usage.used)
57+
self.assertGreater(usage.total, usage.free)
58+
59+
def test_disk_usage_accepts_path_as_a_string(self):
60+
usage = shutil.disk_usage(__file__)
61+
self.assertIsInstance(usage, tuple)
62+
63+
def test_disk_usage_accepts_path_as_bytes(self):
64+
usage = shutil.disk_usage(__file__.encode("utf-8"))
65+
self.assertIsInstance(usage, tuple)
66+
67+
def test_disk_usage_accepts_path_as_a_pathlike(self):
68+
usage = shutil.disk_usage(pathlib.PurePath(__file__))
69+
self.assertIsInstance(usage, tuple)
70+
71+
def test_disk_usage_accepts_file_descriptor(self):
72+
with io.open(__file__) as file:
73+
fd = file.fileno()
74+
usage = shutil.disk_usage(fd)
75+
self.assertIsInstance(usage, tuple)
76+
77+
def test_disk_usage_supports_path_to_file(self):
78+
usage = shutil.disk_usage(__file__)
79+
self.assertIsInstance(usage, tuple)
80+
81+
def test_disk_usage_supports_path_to_directory(self):
82+
usage = shutil.disk_usage(os.path.dirname(__file__))
83+
self.assertIsInstance(usage, tuple)
84+
85+
def test_disk_usage_raises_exception_when_given_path_type_is_not_supported(self):
86+
with self.assertRaisesRegex(TypeError, r'path should be string, bytes, os.PathLike or integer, not list'):
87+
shutil.disk_usage([])
88+
89+
def test_disk_usage_raises_exception_when_given_path_does_not_exist(self):
90+
not_existing_path = __file__ + '.not-existing'
91+
not_existing_file_name = os.path.basename(not_existing_path)
92+
93+
with self.assertRaisesRegex(FileNotFoundError, rf"No such file or directory: '.+{not_existing_file_name}'"):
94+
shutil.disk_usage(not_existing_path)
95+
96+
def test_disk_usage_raises_exception_when_there_is_no_open_file_with_given_file_descriptor(self):
97+
not_existing_file_descriptor = 1_000_000 # expect the current process to not open 1M files
98+
99+
with self.assertRaisesRegex(OSError, r'(B|b)ad file descriptor: 1000000'):
100+
shutil.disk_usage(not_existing_file_descriptor)

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/PosixModuleBuiltins.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,6 @@ public void postInitialize(Python3Core core) {
355355
((PDict) environAttr).setDictStorage(environ.getDictStorage());
356356

357357
if (posixLib.getBackend(posixSupport).toJavaStringUncached().equals("java")) {
358-
posix.setAttribute(toTruffleStringUncached("statvfs"), PNone.NO_VALUE);
359358
posix.setAttribute(toTruffleStringUncached("geteuid"), PNone.NO_VALUE);
360359
posix.setAttribute(toTruffleStringUncached("getegid"), PNone.NO_VALUE);
361360

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/multiprocessing/MultiprocessingGraalPyModuleBuiltins.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ PNone doit(VirtualFrame frame, TruffleString name,
127127
@Cached PConstructAndRaiseNode.Lazy constructAndRaiseNode) {
128128
Semaphore prev = getContext().getSharedMultiprocessingData().removeNamedSemaphore(name);
129129
if (prev == null) {
130-
throw constructAndRaiseNode.get(inliningTarget).raiseFileNotFoundError(frame, ErrorMessages.NO_SUCH_FILE_OR_DIR, "semaphores", name);
130+
throw constructAndRaiseNode.get(inliningTarget).raiseFileNotFoundError(frame, ErrorMessages.NO_SUCH_FILE_OR_DIR_WITH_LABEL, "semaphores", name);
131131
}
132132
return PNone.NONE;
133133
}

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/ErrorMessages.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,8 @@ public abstract class ErrorMessages {
580580
public static final TruffleString NO_CURRENT_FRAME = tsLiteral("%s: no current frame");
581581
public static final TruffleString NO_FUNCTION_FOUND = tsLiteral("no function %s%s found in %s");
582582
public static final TruffleString NO_LOCALS_FOUND = tsLiteral("no locals found");
583-
public static final TruffleString NO_SUCH_FILE_OR_DIR = tsLiteral("No such file or directory: '%s:/%s'");
583+
public static final TruffleString NO_SUCH_FILE_OR_DIR = tsLiteral("No such file or directory");
584+
public static final TruffleString NO_SUCH_FILE_OR_DIR_WITH_LABEL = tsLiteral("No such file or directory: '%s:/%s'");
584585
public static final TruffleString NO_SUCH_NAME = tsLiteral("no such name");
585586
public static final TruffleString NONEMPTY_SLOTS_NOT_ALLOWED_FOR_SUBTYPE_OF_S = tsLiteral("nonempty __slots__ not supported for subtype of '%s'");
586587
public static final TruffleString NON_HEX_DIGIT_FOUND = tsLiteral("Non-hexadecimal digit found");

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/EmulatedPosixSupport.java

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@
188188
import java.nio.file.DirectoryIteratorException;
189189
import java.nio.file.DirectoryStream;
190190
import java.nio.file.LinkOption;
191+
import java.nio.file.NoSuchFileException;
191192
import java.nio.file.StandardCopyOption;
192193
import java.nio.file.StandardOpenOption;
193194
import java.nio.file.attribute.FileAttribute;
@@ -210,6 +211,7 @@
210211
import java.util.concurrent.TimeUnit;
211212
import java.util.logging.Level;
212213

214+
import com.oracle.graal.python.nodes.ErrorMessages;
213215
import org.graalvm.nativeimage.ImageInfo;
214216
import org.graalvm.nativeimage.ProcessProperties;
215217
import org.graalvm.polyglot.io.ProcessHandler.Redirect;
@@ -1076,15 +1078,54 @@ public long[] fstat(int fd,
10761078
}
10771079

10781080
@ExportMessage
1081+
@TruffleBoundary
10791082
@SuppressWarnings("static-method")
1080-
public long[] statvfs(Object path) {
1081-
throw createUnsupportedFeature("statvfs");
1083+
public long[] statvfs(Object path) throws PosixException {
1084+
try {
1085+
Env env = PythonContext.get(null).getEnv();
1086+
TruffleFile truffleFile = env.getPublicTruffleFile((String) path);
1087+
TruffleFile.FileStoreInfo fileStoreInfo = truffleFile.getFileStoreInfo();
1088+
1089+
long totalSpace, usableSpace, unallocatedSpace, blockSize;
1090+
1091+
totalSpace = fileStoreInfo.getTotalSpace();
1092+
usableSpace = fileStoreInfo.getUsableSpace();
1093+
unallocatedSpace = fileStoreInfo.getUnallocatedSpace();
1094+
blockSize = fileStoreInfo.getBlockSize();
1095+
1096+
long bsize, frsize, blocks, bfree, bavail, files, ffree, favail, flag, namemax, fsid;
1097+
1098+
bsize = blockSize; // file system block size
1099+
frsize = blockSize; // fragment size
1100+
blocks = totalSpace / blockSize; // size of fs in f_frsize units
1101+
bfree = unallocatedSpace / blockSize; // free blocks
1102+
bavail = usableSpace / blockSize; // free blocks for unprivileged users
1103+
files = 0; // inodes
1104+
ffree = 0; // free inodes
1105+
favail = 0; // free inodes for unprivileged users
1106+
flag = 0; // mount flags
1107+
namemax = 0; // maximum filename length
1108+
fsid = 0; // file system ID
1109+
1110+
return new long[]{bsize, frsize, blocks, bfree, bavail, files, ffree, favail, flag, namemax, fsid};
1111+
} catch (NoSuchFileException e) {
1112+
throw new PosixException(OSErrorEnum.ENOENT.getNumber(), ErrorMessages.NO_SUCH_FILE_OR_DIR);
1113+
} catch (UnsupportedOperationException | IOException | SecurityException e) {
1114+
TruffleString message = PythonUtils.toTruffleStringUncached(e.getMessage());
1115+
throw new PosixException(OSErrorEnum.EPERM.getNumber(), message);
1116+
}
10821117
}
10831118

10841119
@ExportMessage
10851120
@SuppressWarnings("static-method")
1086-
public long[] fstatvfs(int fd) {
1087-
throw createUnsupportedFeature("fstatvfs");
1121+
public long[] fstatvfs(int fd) throws PosixException {
1122+
String path = getFilePath(fd);
1123+
1124+
if (path == null) {
1125+
throw new PosixException(OSErrorEnum.EBADF.getNumber(), ErrorMessages.BAD_FILE_DESCRIPTOR);
1126+
}
1127+
1128+
return statvfs(path);
10881129
}
10891130

10901131
private static long[] fstatWithoutPath(Channel fileChannel) {

0 commit comments

Comments
 (0)