blob: 54df00d5dc2032ac08945ab4321f019de2b6894a [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.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using Apache.Arrow.Adbc.Drivers.Apache.Spark;
using Apache.Arrow.Adbc.Drivers.Databricks;
using Xunit;
namespace Apache.Arrow.Adbc.Tests.Drivers.Databricks
{
/// <summary>
/// Unit tests for the DatabricksConfiguration class.
/// </summary>
public class DatabricksConfigurationTest : IDisposable
{
private readonly List<string> _tempFiles = new();
/// <summary>
/// Tests that FileNotFoundException is thrown for non-existent files.
/// </summary>
[Fact]
public void FromFile_FileNotFound_ThrowsFileNotFoundException()
{
// Arrange
var nonExistentFile = Path.Combine(Path.GetTempPath(), "nonexistent.json");
// Act & Assert
Assert.Throws<FileNotFoundException>(() => DatabricksConfiguration.FromFile(nonExistentFile));
}
/// <summary>
/// Tests that JsonException is thrown for invalid JSON.
/// </summary>
[Fact]
public void FromFile_InvalidJson_ThrowsJsonException()
{
// Arrange
var invalidJsonFile = CreateTempFile("invalid.json", "{ invalid json");
// Act & Assert
Assert.Throws<JsonException>(() => DatabricksConfiguration.FromFile(invalidJsonFile));
}
/// <summary>
/// Tests loading configuration from empty JSON object.
/// </summary>
[Fact]
public void FromFile_EmptyJson_ReturnsEmptyConfiguration()
{
// Arrange
var emptyJsonFile = CreateTempFile("empty.json", "{}");
// Act
var config = DatabricksConfiguration.FromFile(emptyJsonFile);
// Assert
Assert.NotNull(config);
Assert.NotNull(config.Properties);
Assert.Empty(config.Properties);
}
/// <summary>
/// Tests that JSON with comments is parsed correctly.
/// </summary>
[Fact]
public void FromFile_JsonWithComments_IgnoresComments()
{
// Arrange
var jsonWithComments = $@"{{
// This is a test Databricks configuration
""{SparkParameters.HostName}"": ""test.databricks.com"",
/* Multi-line
comment for auth */
""{SparkParameters.AuthType}"": ""Bearer"",
""{DatabricksParameters.UseCloudFetch}"": ""true""
}}";
var configFile = CreateTempFile("with-comments.json", jsonWithComments);
// Act
var config = DatabricksConfiguration.FromFile(configFile);
// Assert
Assert.NotNull(config);
Assert.Equal(3, config.Properties.Count);
Assert.Equal("test.databricks.com", config.Properties[SparkParameters.HostName]);
Assert.Equal("Bearer", config.Properties[SparkParameters.AuthType]);
Assert.Equal("true", config.Properties[DatabricksParameters.UseCloudFetch]);
}
/// <summary>
/// Tests that JSON with trailing commas is parsed correctly.
/// </summary>
[Fact]
public void FromFile_JsonWithTrailingCommas_ParsesSuccessfully()
{
// Arrange
var jsonWithTrailingCommas = $@"{{
""{SparkParameters.HostName}"": ""test.databricks.com"",
""{DatabricksParameters.MaxBytesPerFile}"": ""10485760"",
}}";
var configFile = CreateTempFile("with-trailing-commas.json", jsonWithTrailingCommas);
// Act
var config = DatabricksConfiguration.FromFile(configFile);
// Assert
Assert.NotNull(config);
Assert.Equal(2, config.Properties.Count);
Assert.Equal("test.databricks.com", config.Properties[SparkParameters.HostName]);
Assert.Equal("10485760", config.Properties[DatabricksParameters.MaxBytesPerFile]);
}
/// <summary>
/// Tests case-insensitive key parsing.
/// </summary>
[Theory]
[InlineData("hostname", "test.databricks.com")]
[InlineData("HOSTNAME", "test.databricks.com")]
[InlineData("HostName", "test.databricks.com")]
[InlineData("authtype", "Bearer")]
[InlineData("AuthType", "Bearer")]
public void FromFile_CaseInsensitiveKeys_ParsesCorrectly(string keyName, string expectedValue)
{
// Arrange
var json = $@"{{ ""{keyName}"": ""{expectedValue}"" }}";
var configFile = CreateTempFile("case-test.json", json);
// Act
var config = DatabricksConfiguration.FromFile(configFile);
// Assert
Assert.NotNull(config);
Assert.Single(config.Properties);
Assert.Contains(config.Properties, kvp => kvp.Value == expectedValue);
}
/// <summary>
/// Tests loading configuration from environment variable.
/// </summary>
[Fact]
public void FromEnvironmentVariable_ValidPath_LoadsConfiguration()
{
// Arrange
var properties = new Dictionary<string, string>
{
[SparkParameters.HostName] = "env.databricks.com",
[SparkParameters.AuthType] = "Bearer",
[DatabricksParameters.EnableDirectResults] = "true"
};
var configFile = CreateTempJsonFile(properties);
var envVar = "TEST_ENV_VAR_" + Guid.NewGuid().ToString("N").Substring(0, 8);
try
{
Environment.SetEnvironmentVariable(envVar, configFile);
// Act
var config = DatabricksConfiguration.FromEnvironmentVariable(envVar);
// Assert
Assert.NotNull(config);
Assert.Equal(properties.Count, config.Properties.Count);
Assert.Equal("env.databricks.com", config.Properties[SparkParameters.HostName]);
Assert.Equal("true", config.Properties[DatabricksParameters.EnableDirectResults]);
}
finally
{
Environment.SetEnvironmentVariable(envVar, null);
}
}
/// <summary>
/// Tests TryFromEnvironmentVariable with invalid configuration.
/// </summary>
[Fact]
public void TryFromEnvironmentVariable_InvalidConfig_ReturnsNull()
{
// Arrange
var nonExistentEnvVar = "NONEXISTENT_TRY_VAR_" + Guid.NewGuid().ToString("N");
// Act
var config = DatabricksConfiguration.TryFromEnvironmentVariable(nonExistentEnvVar);
// Assert
Assert.Null(config);
}
/// <summary>
/// Tests CanLoadFromEnvironment with valid environment variable.
/// </summary>
[Fact]
public void CanLoadFromEnvironment_ValidPath_ReturnsTrue()
{
// Arrange
var configFile = CreateTempJsonFile(new Dictionary<string, string> { ["test"] = "value" });
var envVar = "TEST_CAN_LOAD_" + Guid.NewGuid().ToString("N").Substring(0, 8);
try
{
Environment.SetEnvironmentVariable(envVar, configFile);
// Act
var canLoad = DatabricksConfiguration.CanLoadFromEnvironment(envVar, out string? filePath);
// Assert
Assert.True(canLoad);
Assert.Equal(configFile, filePath);
}
finally
{
Environment.SetEnvironmentVariable(envVar, null);
}
}
/// <summary>
/// Tests CanLoadFromEnvironment with invalid paths.
/// </summary>
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void CanLoadFromEnvironment_InvalidEnvVar_ReturnsFalse(string? envVarName)
{
// Act
var canLoad = DatabricksConfiguration.CanLoadFromEnvironment(envVarName!, out string? filePath);
// Assert
Assert.False(canLoad);
Assert.Null(filePath);
}
/// <summary>
/// Tests CanLoadFromEnvironment with unset environment variable.
/// </summary>
[Fact]
public void CanLoadFromEnvironment_UnsetVar_ReturnsFalse()
{
// Arrange
var unsetEnvVar = "UNSET_ENV_VAR_" + Guid.NewGuid().ToString("N");
// Act
var canLoad = DatabricksConfiguration.CanLoadFromEnvironment(unsetEnvVar, out string? filePath);
// Assert
Assert.False(canLoad);
Assert.Null(filePath);
}
/// <summary>
/// Tests CanLoadFromEnvironment with non-existent file.
/// </summary>
[Fact]
public void CanLoadFromEnvironment_NonExistentFile_ReturnsFalse()
{
// Arrange
var envVar = "TEST_NONEXISTENT_" + Guid.NewGuid().ToString("N").Substring(0, 8);
var nonExistentFile = Path.Combine(Path.GetTempPath(), "nonexistent.json");
try
{
Environment.SetEnvironmentVariable(envVar, nonExistentFile);
// Act
var canLoad = DatabricksConfiguration.CanLoadFromEnvironment(envVar, out string? filePath);
// Assert
Assert.False(canLoad);
Assert.Equal(nonExistentFile, filePath);
}
finally
{
Environment.SetEnvironmentVariable(envVar, null);
}
}
/// <summary>
/// Tests that property values are actually overridden based on precedence settings in DatabricksConnection.
/// Validates that the correct property values (EnablePKFK and UseCloudFetch) are used based on precedence.
/// </summary>
[Theory]
[InlineData("true")] // Environment takes precedence, should use environment values (false, false)
[InlineData("false")] // Constructor takes precedence, should use constructor values (true, true)
public void DatabricksConnection_PropertyOverridePrecedence_ValidatesCorrectValues(string precedenceValue)
{
// Arrange
var envProperties = new Dictionary<string, string>
{
[DatabricksParameters.DriverConfigTakePrecedence] = precedenceValue,
[DatabricksParameters.EnablePKFK] = "false",
[DatabricksParameters.UseCloudFetch] = "false"
};
var configFile = CreateTempJsonFile(envProperties);
var envVar = "TEST_PRECEDENCE_" + Guid.NewGuid().ToString("N").Substring(0, 8);
var constructorProperties = new Dictionary<string, string>
{
[SparkParameters.HostName] = "test-host",
[SparkParameters.Token] = "test-token",
[DatabricksParameters.EnablePKFK] = "true",
[DatabricksParameters.UseCloudFetch] = "true",
[DatabricksParameters.CloudFetchMaxRetries] = "5"
};
try
{
Environment.SetEnvironmentVariable(DatabricksConnection.DefaultConfigEnvironmentVariable, configFile);
// Act
using var connection = new DatabricksConnection(constructorProperties);
// Assert - Validate that the correct property values are being used based on precedence
if (precedenceValue == "true")
{
// Environment takes precedence: should use environment values (false, false)
Assert.False(connection.EnablePKFK, "Environment precedence should use EnablePKFK=false from environment config");
Assert.False(connection.UseCloudFetch, "Environment precedence should use UseCloudFetch=false from environment config");
}
else
{
// Constructor takes precedence: should use constructor values (true, true)
Assert.True(connection.EnablePKFK, "Constructor precedence should use EnablePKFK=true from constructor properties");
Assert.True(connection.UseCloudFetch, "Constructor precedence should use UseCloudFetch=true from constructor properties");
}
}
finally
{
Environment.SetEnvironmentVariable(DatabricksConnection.DefaultConfigEnvironmentVariable, null);
}
}
#region Helper Methods
/// <summary>
/// Creates a temporary JSON file with the specified properties.
/// </summary>
private string CreateTempJsonFile(Dictionary<string, string> properties)
{
var json = JsonSerializer.Serialize(properties, new JsonSerializerOptions { WriteIndented = true });
return CreateTempFile("config.json", json);
}
/// <summary>
/// Creates a temporary file with the specified content.
/// </summary>
private string CreateTempFile(string fileName, string content)
{
var tempFile = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}_{fileName}");
File.WriteAllText(tempFile, content);
_tempFiles.Add(tempFile);
return tempFile;
}
/// <summary>
/// Cleans up temporary files.
/// </summary>
public void Dispose()
{
foreach (var tempFile in _tempFiles)
{
try
{
if (File.Exists(tempFile))
File.Delete(tempFile);
}
catch
{
// Ignore cleanup errors
}
}
}
#endregion
}
}