blob: f261b0a73bc9d6951fcce9839bba0de4f15703fd [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import os
import shutil
import tempfile
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
import pyarrow.fs as pafs
from pypaimon.common.file_io import create_temp_path
from pypaimon.common.options import Options
from pypaimon.common.options.config import OssOptions
from pypaimon.filesystem.local_file_io import LocalFileIO
from pypaimon.filesystem.pyarrow_file_io import PyArrowFileIO, _pyarrow_lt_7
class FileIOTest(unittest.TestCase):
"""Test cases for FileIO.to_filesystem_path method."""
@patch('pypaimon.common.file_io.uuid.uuid4', return_value='test-uuid')
def test_create_temp_path(self, _):
self.assertEqual(
create_temp_path("oss://bucket/table/snapshot/snapshot-1"),
"oss://bucket/table/snapshot/.snapshot-1.test-uuid.tmp")
self.assertEqual(
create_temp_path("snapshot-1"),
".snapshot-1.test-uuid.tmp")
self.assertEqual(
create_temp_path(r"C:\table\snapshot\snapshot-1"),
r"C:\table\snapshot\.snapshot-1.test-uuid.tmp")
def test_filesystem_path_conversion(self):
"""Test S3FileSystem path conversion with various formats."""
file_io = PyArrowFileIO("s3://bucket/warehouse", Options({}))
self.assertIsInstance(file_io.filesystem, pafs.S3FileSystem)
# Test bucket and path
self.assertEqual(file_io.to_filesystem_path("s3://my-bucket/path/to/file.txt"),
"my-bucket/path/to/file.txt")
self.assertEqual(file_io.to_filesystem_path("oss://my-bucket/path/to/file.txt"),
"my-bucket/path/to/file.txt")
# Test bucket only
self.assertEqual(file_io.to_filesystem_path("s3://my-bucket"), "my-bucket")
self.assertEqual(file_io.to_filesystem_path("oss://my-bucket"), "my-bucket")
# Test scheme but no netloc
self.assertEqual(file_io.to_filesystem_path("oss:///path/to/file.txt"), "path/to/file.txt")
self.assertEqual(file_io.to_filesystem_path("s3:///path/to/file.txt"), "path/to/file.txt")
# Test empty path
self.assertEqual(file_io.to_filesystem_path("oss:///"), ".")
# Test path without scheme
self.assertEqual(file_io.to_filesystem_path("bucket/path/to/file.txt"),
"bucket/path/to/file.txt")
# Test idempotency
converted_path = "my-bucket/path/to/file.txt"
self.assertEqual(file_io.to_filesystem_path(converted_path), converted_path)
parent_str = str(Path(converted_path).parent)
self.assertEqual(file_io.to_filesystem_path(parent_str), parent_str)
oss_io = PyArrowFileIO("oss://test-bucket/warehouse", Options({
OssOptions.OSS_ENDPOINT.key(): 'oss-cn-hangzhou.aliyuncs.com',
OssOptions.OSS_ACCESS_KEY_ID.key(): 'test-key',
OssOptions.OSS_ACCESS_KEY_SECRET.key(): 'test-secret',
OssOptions.OSS_IMPL.key(): 'legacy',
}))
# PyArrow <16 bakes the bucket into endpoint_override, so keys omit it.
bucket_stripped = oss_io._oss_bucket_in_endpoint
got = oss_io.to_filesystem_path("oss://test-bucket/path/to/file.txt")
self.assertEqual(got, "path/to/file.txt" if bucket_stripped else "test-bucket/path/to/file.txt")
if bucket_stripped:
self.assertEqual(oss_io.to_filesystem_path("db-xxx.db/tbl-xxx/data.parquet"),
"db-xxx.db/tbl-xxx/data.parquet")
self.assertEqual(oss_io.to_filesystem_path("db-xxx.db/tbl-xxx"), "db-xxx.db/tbl-xxx")
manifest_uri = "oss://test-bucket/warehouse/db.db/table/manifest/manifest-list-abc-0"
manifest_key = oss_io.to_filesystem_path(manifest_uri)
self.assertEqual(manifest_key, "warehouse/db.db/table/manifest/manifest-list-abc-0",
"OSS+PyArrow<16 must pass key only so manifest lands in the right bucket")
self.assertFalse(manifest_key.startswith("test-bucket/"),
"path must not start with bucket or PyArrow <16 writes to wrong bucket")
nf = MagicMock(type=pafs.FileType.NotFound)
get_file_info_calls = []
def record_get_file_info(paths):
get_file_info_calls.append(list(paths))
return [MagicMock(type=pafs.FileType.NotFound) for _ in paths]
mock_fs = MagicMock()
mock_fs.get_file_info.side_effect = record_get_file_info if bucket_stripped else [[nf], [nf]]
mock_fs.create_dir = MagicMock()
mock_fs.open_output_stream.return_value = MagicMock()
oss_io.filesystem = mock_fs
# Bucket-in-endpoint mode (PyArrow < 16) mkdirs probes the real bucket
# over HTTP; keep the test offline.
with patch("requests.get", return_value=MagicMock(status_code=403)):
oss_io.new_output_stream("oss://test-bucket/path/to/file.txt")
if bucket_stripped:
# Legacy mkdirs must not create_dir (it would CreateBucket the
# first key segment and corrupt the parent directory).
mock_fs.create_dir.assert_not_called()
else:
mock_fs.create_dir.assert_called_once()
path_str = oss_io.to_filesystem_path("oss://test-bucket/path/to/file.txt")
expected_parent = "/".join(path_str.split("/")[:-1]) if "/" in path_str else str(Path(path_str).parent)
self.assertEqual(mock_fs.create_dir.call_args[0][0], expected_parent)
if bucket_stripped:
for call_paths in get_file_info_calls:
for p in call_paths:
self.assertFalse(
p.startswith("test-bucket/"),
"OSS+PyArrow<16 must pass key only to get_file_info, not bucket/key. Got: %r" % (p,)
)
def test_exists(self):
lt7 = _pyarrow_lt_7()
with tempfile.TemporaryDirectory(prefix="file_io_nonexistent_") as tmpdir:
file_io = LocalFileIO("file://" + tmpdir, Options({}))
missing_uri = "file://" + os.path.join(tmpdir, "nonexistent_xyz")
path_str = file_io.to_filesystem_path(missing_uri)
raised = None
infos = None
try:
infos = file_io.filesystem.get_file_info([path_str])
except OSError as e:
raised = e
if lt7:
if raised is not None:
err = str(raised).lower()
self.assertTrue("133" in err or "does not exist" in err or "not exist" in err, str(raised))
else:
self.assertEqual(len(infos), 1)
self.assertEqual(infos[0].type, pafs.FileType.NotFound)
else:
self.assertIsNone(raised)
self.assertEqual(len(infos), 1)
self.assertEqual(infos[0].type, pafs.FileType.NotFound)
self.assertFalse(file_io.exists(missing_uri))
with self.assertRaises(FileNotFoundError):
file_io.get_file_status(missing_uri)
def test_local_filesystem_path_conversion(self):
file_io = LocalFileIO("file:///tmp/warehouse", Options({}))
self.assertIsInstance(file_io, LocalFileIO)
# Test file:// scheme
self.assertEqual(file_io.to_filesystem_path("file:///tmp/path/to/file.txt"),
"/tmp/path/to/file.txt")
self.assertEqual(file_io.to_filesystem_path("file:///path/to/file.txt"),
"/path/to/file.txt")
# Test empty paths
self.assertEqual(file_io.to_filesystem_path("file://"), ".")
self.assertEqual(file_io.to_filesystem_path("file:///"), "/")
# Test paths without scheme
self.assertEqual(file_io.to_filesystem_path("/tmp/path/to/file.txt"),
"/tmp/path/to/file.txt")
self.assertEqual(file_io.to_filesystem_path("relative/path/to/file.txt"),
"relative/path/to/file.txt")
self.assertEqual(file_io.to_filesystem_path("./relative/path/to/file.txt"),
"./relative/path/to/file.txt")
# Test idempotency
converted_path = "/tmp/path/to/file.txt"
self.assertEqual(file_io.to_filesystem_path(converted_path), converted_path)
parent_str = str(Path(converted_path).parent)
self.assertEqual(file_io.to_filesystem_path(parent_str), parent_str)
def test_windows_path_handling(self):
file_io = LocalFileIO("file:///tmp/warehouse", Options({}))
self.assertIsInstance(file_io, LocalFileIO)
self.assertEqual(file_io.to_filesystem_path("C:\\path\\to\\file.txt"),
"C:\\path\\to\\file.txt")
self.assertEqual(file_io.to_filesystem_path("C:/path/to/file.txt"),
"C:/path/to/file.txt")
self.assertEqual(file_io.to_filesystem_path("C:"), "C:")
# file:// scheme with Windows drive
self.assertEqual(file_io.to_filesystem_path("file://C:/path/to/file.txt"),
"C:/path/to/file.txt")
self.assertEqual(file_io.to_filesystem_path("file://C:/path"), "C:/path")
self.assertEqual(file_io.to_filesystem_path("file://C:"), "C:")
self.assertEqual(file_io.to_filesystem_path("file:///C:/path/to/file.txt"),
"/C:/path/to/file.txt")
# Windows path with S3FileSystem (should preserve)
s3_file_io = PyArrowFileIO("s3://bucket/warehouse", Options({}))
self.assertEqual(s3_file_io.to_filesystem_path("C:\\path\\to\\file.txt"),
"C:\\path\\to\\file.txt")
def test_path_normalization(self):
"""Test path normalization (multiple slashes)."""
file_io = LocalFileIO("file:///tmp/warehouse", Options({}))
self.assertEqual(file_io.to_filesystem_path("file://///tmp///path///file.txt"),
"/tmp/path/file.txt")
s3_file_io = PyArrowFileIO("s3://bucket/warehouse", Options({}))
self.assertEqual(s3_file_io.to_filesystem_path("s3://my-bucket///path///to///file.txt"),
"my-bucket/path/to/file.txt")
def test_write_file_with_overwrite_flag(self):
temp_dir = tempfile.mkdtemp(prefix="file_io_write_test_")
try:
warehouse_path = f"file://{temp_dir}"
file_io = LocalFileIO(warehouse_path, Options({}))
test_file_uri = f"file://{temp_dir}/overwrite_test.txt"
expected_path = os.path.join(temp_dir, "overwrite_test.txt")
# 1) Write to a new file with default overwrite=False
file_io.write_file(test_file_uri, "first content")
self.assertTrue(os.path.exists(expected_path))
with open(expected_path, "r", encoding="utf-8") as f:
self.assertEqual(f.read(), "first content")
# 2) Attempt to write again with overwrite=False should raise FileExistsError
with self.assertRaises(FileExistsError):
file_io.write_file(test_file_uri, "second content", overwrite=False)
# Ensure content is unchanged
with open(expected_path, "r", encoding="utf-8") as f:
self.assertEqual(f.read(), "first content")
# 3) Write with overwrite=True should replace the content
file_io.write_file(test_file_uri, "overwritten content", overwrite=True)
with open(expected_path, "r", encoding="utf-8") as f:
self.assertEqual(f.read(), "overwritten content")
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def test_exists_does_not_catch_exception(self):
temp_dir = tempfile.mkdtemp(prefix="file_io_exists_test_")
try:
warehouse_path = f"file://{temp_dir}"
file_io = LocalFileIO(warehouse_path, Options({}))
test_file = os.path.join(temp_dir, "test_file.txt")
with open(test_file, "w") as f:
f.write("test")
self.assertTrue(file_io.exists(f"file://{test_file}"))
self.assertFalse(file_io.exists(f"file://{temp_dir}/nonexistent.txt"))
mock_path = MagicMock(spec=Path)
mock_path.exists.side_effect = OSError("Permission denied")
with patch.object(file_io, '_to_file', return_value=mock_path):
with self.assertRaises(OSError) as context:
file_io.exists("file:///some/path")
self.assertIn("Permission denied", str(context.exception))
with patch('builtins.open', side_effect=OSError("Permission denied")):
with self.assertRaises(OSError):
file_io.new_output_stream("file:///some/path/file.txt")
mock_path = MagicMock(spec=Path)
mock_path.is_dir.side_effect = OSError("Permission denied")
with patch.object(file_io, '_to_file', return_value=mock_path):
with self.assertRaises(OSError):
file_io.check_or_mkdirs("file:///some/path")
with patch('builtins.open', side_effect=OSError("Permission denied")):
with self.assertRaises(OSError):
file_io.write_file("file:///some/path", "content", overwrite=False)
with patch('builtins.open', side_effect=OSError("Permission denied")):
with self.assertRaises(OSError):
file_io.copy_file("file:///src", "file:///dst", overwrite=False)
with patch.object(file_io, 'exists', return_value=True):
with patch.object(file_io, 'read_file_utf8', side_effect=OSError("Read error")):
with self.assertRaises(OSError) as context:
file_io.read_overwritten_file_utf8("file:///some/path")
self.assertIn("Read error", str(context.exception))
# rename() catches OSError and returns False (consistent with Java implementation)
mock_src_path = MagicMock(spec=Path)
mock_dst_path = MagicMock(spec=Path)
mock_dst_path.parent = MagicMock()
mock_dst_path.parent.exists.return_value = True
mock_dst_path.exists.return_value = False
mock_src_path.rename.side_effect = OSError("Network error")
with patch.object(file_io, '_to_file', side_effect=[mock_src_path, mock_dst_path]):
# rename() catches OSError and returns False, doesn't raise
result = file_io.rename("file:///src", "file:///dst")
self.assertFalse(result, "rename() should return False when OSError occurs")
file_io.delete_quietly("file:///some/path")
file_io.delete_directory_quietly("file:///some/path")
oss_io = PyArrowFileIO("oss://test-bucket/warehouse", Options({
OssOptions.OSS_ENDPOINT.key(): 'oss-cn-hangzhou.aliyuncs.com',
OssOptions.OSS_ACCESS_KEY_ID.key(): 'test-key',
OssOptions.OSS_ACCESS_KEY_SECRET.key(): 'test-secret',
OssOptions.OSS_IMPL.key(): 'legacy',
}))
mock_fs = MagicMock()
mock_fs.get_file_info.return_value = [
MagicMock(type=pafs.FileType.NotFound)]
oss_io.filesystem = mock_fs
self.assertFalse(oss_io.exists("oss://test-bucket/path/to/file.txt"))
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def test_delete_non_empty_directory_raises_error(self):
temp_dir = tempfile.mkdtemp(prefix="file_io_delete_test_")
try:
warehouse_path = f"file://{temp_dir}"
file_io = LocalFileIO(warehouse_path, Options({}))
test_dir = os.path.join(temp_dir, "test_dir")
os.makedirs(test_dir)
test_file = os.path.join(test_dir, "test_file.txt")
with open(test_file, "w") as f:
f.write("test")
with self.assertRaises(OSError) as context:
file_io.delete(f"file://{test_dir}", recursive=False)
self.assertIn("is not empty", str(context.exception))
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def test_delete_returns_false_when_file_not_exists(self):
temp_dir = tempfile.mkdtemp(prefix="file_io_delete_test_")
try:
warehouse_path = f"file://{temp_dir}"
file_io = LocalFileIO(warehouse_path, Options({}))
result = file_io.delete(f"file://{temp_dir}/nonexistent.txt")
self.assertFalse(result, "delete() should return False when file does not exist")
result = file_io.delete(f"file://{temp_dir}/nonexistent_dir", recursive=False)
self.assertFalse(result, "delete() should return False when directory does not exist")
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def test_mkdirs_raises_error_when_path_is_file(self):
temp_dir = tempfile.mkdtemp(prefix="file_io_mkdirs_test_")
try:
warehouse_path = f"file://{temp_dir}"
file_io = LocalFileIO(warehouse_path, Options({}))
test_file = os.path.join(temp_dir, "test_file.txt")
with open(test_file, "w") as f:
f.write("test")
with self.assertRaises(FileExistsError) as context:
file_io.mkdirs(f"file://{test_file}")
self.assertIn("is not a directory", str(context.exception))
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def test_rename_returns_false_when_dst_exists(self):
temp_dir = tempfile.mkdtemp(prefix="file_io_rename_test_")
try:
warehouse_path = f"file://{temp_dir}"
file_io = LocalFileIO(warehouse_path, Options({}))
src_file = os.path.join(temp_dir, "src.txt")
dst_file = os.path.join(temp_dir, "dst.txt")
with open(src_file, "w") as f:
f.write("src")
with open(dst_file, "w") as f:
f.write("dst")
result = file_io.rename(f"file://{src_file}", f"file://{dst_file}")
self.assertFalse(result)
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def test_get_file_status_raises_error_when_file_not_exists(self):
temp_dir = tempfile.mkdtemp(prefix="file_io_get_file_status_test_")
try:
warehouse_path = f"file://{temp_dir}"
file_io = LocalFileIO(warehouse_path, Options({}))
with self.assertRaises(FileNotFoundError) as context:
file_io.get_file_status(f"file://{temp_dir}/nonexistent.txt")
self.assertIn("does not exist", str(context.exception))
test_file = os.path.join(temp_dir, "test_file.txt")
with open(test_file, "w") as f:
f.write("test content")
file_info = file_io.get_file_status(f"file://{test_file}")
self.assertEqual(file_info.type, pafs.FileType.File)
self.assertIsNotNone(file_info.size)
with self.assertRaises(FileNotFoundError) as context:
file_io.get_file_size(f"file://{temp_dir}/nonexistent.txt")
self.assertIn("does not exist", str(context.exception))
with self.assertRaises(FileNotFoundError) as context:
file_io.is_dir(f"file://{temp_dir}/nonexistent_dir")
self.assertIn("does not exist", str(context.exception))
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def test_copy_file(self):
temp_dir = tempfile.mkdtemp(prefix="file_io_copy_test_")
try:
warehouse_path = f"file://{temp_dir}"
file_io = LocalFileIO(warehouse_path, Options({}))
source_file = os.path.join(temp_dir, "source.txt")
target_file = os.path.join(temp_dir, "target.txt")
with open(source_file, "w") as f:
f.write("source content")
# Test 1: Raises FileExistsError when target exists and overwrite=False
with open(target_file, "w") as f:
f.write("target content")
with self.assertRaises(FileExistsError) as context:
file_io.copy_file(f"file://{source_file}", f"file://{target_file}", overwrite=False)
self.assertIn("already exists", str(context.exception))
with open(target_file, "r") as f:
self.assertEqual(f.read(), "target content")
# Test 2: Overwrites when overwrite=True
file_io.copy_file(f"file://{source_file}", f"file://{target_file}", overwrite=True)
with open(target_file, "r") as f:
self.assertEqual(f.read(), "source content")
# Test 3: Creates parent directory if it doesn't exist
target_file_in_subdir = os.path.join(temp_dir, "subdir", "target.txt")
file_io.copy_file(f"file://{source_file}", f"file://{target_file_in_subdir}", overwrite=False)
self.assertTrue(os.path.exists(target_file_in_subdir))
with open(target_file_in_subdir, "r") as f:
self.assertEqual(f.read(), "source content")
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def test_try_to_write_atomic(self):
temp_dir = tempfile.mkdtemp(prefix="file_io_try_write_atomic_test_")
try:
target_dir = os.path.join(temp_dir, "target_dir")
normal_file = os.path.join(temp_dir, "normal_file.txt")
from pypaimon.filesystem.local_file_io import LocalFileIO
local_file_io = LocalFileIO(f"file://{temp_dir}", Options({}))
os.makedirs(target_dir)
self.assertFalse(
local_file_io.try_to_write_atomic(f"file://{target_dir}", "test content"),
"LocalFileIO should return False when target is a directory")
self.assertEqual(len(os.listdir(target_dir)), 0, "No file should be created inside the directory")
self.assertTrue(local_file_io.try_to_write_atomic(f"file://{normal_file}", "test content"))
with open(normal_file, "r") as f:
self.assertEqual(f.read(), "test content")
os.remove(normal_file)
local_file_io = LocalFileIO(f"file://{temp_dir}", Options({}))
self.assertFalse(
local_file_io.try_to_write_atomic(f"file://{target_dir}", "test content"),
"LocalFileIO should return False when target is a directory")
self.assertEqual(len(os.listdir(target_dir)), 0, "No file should be created inside the directory")
self.assertTrue(local_file_io.try_to_write_atomic(f"file://{normal_file}", "test content"))
with open(normal_file, "r") as f:
self.assertEqual(f.read(), "test content")
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def test_path_on_windows(self):
oss_io = PyArrowFileIO("oss://test-bucket/warehouse", Options({
OssOptions.OSS_ENDPOINT.key(): 'oss-cn-hangzhou.aliyuncs.com',
OssOptions.OSS_ACCESS_KEY_ID.key(): 'test-key',
OssOptions.OSS_ACCESS_KEY_SECRET.key(): 'test-secret',
OssOptions.OSS_IMPL.key(): 'legacy',
}))
mock_fs = MagicMock()
mock_fs.get_file_info.return_value = [MagicMock(type=pafs.FileType.NotFound)]
mock_fs.create_dir = MagicMock()
mock_fs.open_output_stream.return_value = MagicMock()
mock_fs.move = MagicMock()
mock_fs.copy_file = MagicMock()
oss_io.filesystem = mock_fs
# PyArrow < 16 mkdirs probes the real bucket over HTTP; keep the test offline.
with patch("requests.get", return_value=MagicMock(status_code=403)):
oss_io.new_output_stream("oss://test-bucket/db.db/tbl/bucket-0/data.parquet")
oss_io.rename("oss://test-bucket/db.db/tbl/old.parquet",
"oss://test-bucket/db.db/tbl/new.parquet")
oss_io.copy_file("oss://test-bucket/db.db/tbl/src.parquet",
"oss://test-bucket/db.db/tbl/dst.parquet")
for call in mock_fs.create_dir.call_args_list:
self.assertNotIn("\\", call[0][0], f"backslash in path: {call[0][0]}")
class HdfsFileIOTest(unittest.TestCase):
"""Cases for HDFS / ViewFS URI handling in PyArrowFileIO._initialize_hdfs_fs."""
def _make_hdfs_env(self, env_patch):
env_patch.setdefault('HADOOP_HOME', '/opt/hadoop')
env_patch.setdefault('HADOOP_CONF_DIR', '/opt/hadoop/etc/hadoop')
env_patch.setdefault('CLASSPATH', '')
env_patch.setdefault('LD_LIBRARY_PATH', '')
return env_patch
def _make_file_io(self):
file_io = PyArrowFileIO.__new__(PyArrowFileIO)
file_io.properties = Options({})
return file_io
@patch('pypaimon.filesystem.pyarrow_file_io.subprocess.run')
@patch('pypaimon.filesystem.pyarrow_file_io.pafs.HadoopFileSystem')
def test_viewfs_uses_default_host(self, mock_hadoop_fs, mock_run):
mock_run.return_value = MagicMock(stdout='/opt/hadoop/share/hadoop/common/*')
mock_hadoop_fs.return_value = MagicMock()
with patch.dict(os.environ, self._make_hdfs_env({}), clear=True):
self._make_file_io()._initialize_hdfs_fs('viewfs', 'clusterName')
mock_hadoop_fs.assert_called_once_with(host='default', port=0, user='hadoop')
@patch('pypaimon.filesystem.pyarrow_file_io.subprocess.run')
@patch('pypaimon.filesystem.pyarrow_file_io.pafs.HadoopFileSystem')
def test_viewfs_without_netloc_uses_default_host(self, mock_hadoop_fs, mock_run):
mock_run.return_value = MagicMock(stdout='/opt/hadoop/share/hadoop/common/*')
mock_hadoop_fs.return_value = MagicMock()
with patch.dict(os.environ, self._make_hdfs_env({}), clear=True):
self._make_file_io()._initialize_hdfs_fs('viewfs', '')
mock_hadoop_fs.assert_called_once_with(host='default', port=0, user='hadoop')
@patch('pypaimon.filesystem.pyarrow_file_io.subprocess.run')
@patch('pypaimon.filesystem.pyarrow_file_io.pafs.HadoopFileSystem')
def test_hdfs_with_port_uses_explicit_host(self, mock_hadoop_fs, mock_run):
mock_run.return_value = MagicMock(stdout='/opt/hadoop/share/hadoop/common/*')
mock_hadoop_fs.return_value = MagicMock()
with patch.dict(os.environ, self._make_hdfs_env({}), clear=True):
self._make_file_io()._initialize_hdfs_fs('hdfs', 'namenode:8020')
mock_hadoop_fs.assert_called_once_with(host='namenode', port=8020, user='hadoop')
@patch('pypaimon.filesystem.pyarrow_file_io.subprocess.run')
@patch('pypaimon.filesystem.pyarrow_file_io.pafs.HadoopFileSystem')
def test_hdfs_ha_nameservice_without_port_uses_default_host(self, mock_hadoop_fs, mock_run):
mock_run.return_value = MagicMock(stdout='/opt/hadoop/share/hadoop/common/*')
mock_hadoop_fs.return_value = MagicMock()
with patch.dict(os.environ, self._make_hdfs_env({}), clear=True):
self._make_file_io()._initialize_hdfs_fs('hdfs', 'nameservice1')
mock_hadoop_fs.assert_called_once_with(host='default', port=0, user='hadoop')
@patch('pypaimon.filesystem.pyarrow_file_io.subprocess.run')
@patch('pypaimon.filesystem.pyarrow_file_io.pafs.HadoopFileSystem')
def test_hdfs_without_netloc_uses_default_host(self, mock_hadoop_fs, mock_run):
mock_run.return_value = MagicMock(stdout='/opt/hadoop/share/hadoop/common/*')
mock_hadoop_fs.return_value = MagicMock()
with patch.dict(os.environ, self._make_hdfs_env({}), clear=True):
self._make_file_io()._initialize_hdfs_fs('hdfs', '')
mock_hadoop_fs.assert_called_once_with(host='default', port=0, user='hadoop')
def test_hdfs_missing_hadoop_home_raises(self):
with patch.dict(os.environ, {'HADOOP_CONF_DIR': '/opt/hadoop/etc/hadoop'}, clear=True):
with self.assertRaises(RuntimeError) as ctx:
self._make_file_io()._initialize_hdfs_fs('hdfs', 'namenode:8020')
self.assertIn('HADOOP_HOME', str(ctx.exception))
def test_hdfs_missing_hadoop_conf_dir_raises(self):
with patch.dict(os.environ, {'HADOOP_HOME': '/opt/hadoop'}, clear=True):
with self.assertRaises(RuntimeError) as ctx:
self._make_file_io()._initialize_hdfs_fs('hdfs', 'namenode:8020')
self.assertIn('HADOOP_CONF_DIR', str(ctx.exception))
@unittest.skipUnless(hasattr(pafs, 'GcsFileSystem'), "PyArrow GCS support not available")
class GCSFileIOPathTest(unittest.TestCase):
"""Unit tests for PyArrowFileIO.to_filesystem_path with GCS (no credentials required)."""
def setUp(self):
self.file_io = PyArrowFileIO("gs://my-bucket/warehouse", Options({}))
def test_gcs_filesystem_type(self):
"""GCS warehouse path should produce a GcsFileSystem."""
self.assertIsInstance(self.file_io.filesystem, pafs.GcsFileSystem)
def test_gcs_path_conversion(self):
"""gs://bucket/key should map to bucket/key (no leading slash)."""
self.assertEqual(
self.file_io.to_filesystem_path("gs://my-bucket/path/to/file.parquet"),
"my-bucket/path/to/file.parquet"
)
self.assertEqual(
self.file_io.to_filesystem_path("gs://my-bucket/warehouse/db.db/tbl/data-0.parquet"),
"my-bucket/warehouse/db.db/tbl/data-0.parquet"
)
def test_gcs_path_bucket_only(self):
"""gs://bucket with no path should return just the bucket name."""
self.assertEqual(
self.file_io.to_filesystem_path("gs://my-bucket"),
"my-bucket"
)
def test_gcs_path_normalization(self):
"""Multiple consecutive slashes in the path should be collapsed."""
self.assertEqual(
self.file_io.to_filesystem_path("gs://my-bucket///path///to///file.parquet"),
"my-bucket/path/to/file.parquet"
)
def test_gcs_path_idempotency(self):
"""Already-converted bucket/key paths should pass through unchanged."""
converted = "my-bucket/path/to/file.parquet"
self.assertEqual(self.file_io.to_filesystem_path(converted), converted)
parent = str(Path(converted).parent)
self.assertEqual(self.file_io.to_filesystem_path(parent), parent)
def test_gcs_path_no_leading_slash(self):
"""to_filesystem_path must never return a path starting with '/'."""
cases = [
"gs://my-bucket/path/to/file.parquet",
"gs://my-bucket",
"gs://my-bucket///path",
]
for uri in cases:
result = self.file_io.to_filesystem_path(uri)
self.assertFalse(
result.startswith("/"),
f"Path must not start with '/' for GcsFileSystem, got: {result!r} (input: {uri!r})"
)
if __name__ == '__main__':
unittest.main()