blob: a9489cfd98ee61ded39a2958d41d1d4d2bd5d739 [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.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."""
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)
lt7 = _pyarrow_lt_7()
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',
}))
got = oss_io.to_filesystem_path("oss://test-bucket/path/to/file.txt")
self.assertEqual(got, "path/to/file.txt" if lt7 else "test-bucket/path/to/file.txt")
if lt7:
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+PyArrow6 must pass key only to PyArrow so manifest is written to correct bucket")
self.assertFalse(manifest_key.startswith("test-bucket/"),
"path must not start with bucket name or PyArrow 6 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 lt7 else [[nf], [nf]]
mock_fs.create_dir = MagicMock()
mock_fs.open_output_stream.return_value = MagicMock()
oss_io.filesystem = mock_fs
oss_io.new_output_stream("oss://test-bucket/path/to/file.txt")
mock_fs.create_dir.assert_called_once()
path_str = oss_io.to_filesystem_path("oss://test-bucket/path/to/file.txt")
if lt7:
expected_parent = '/'.join(path_str.split('/')[:-1]) if '/' in path_str else ''
else:
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 lt7:
for call_paths in get_file_info_calls:
for p in call_paths:
self.assertFalse(
p.startswith("test-bucket/"),
"OSS+PyArrow<7 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
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()