Advanced Integration Scenarios
This comprehensive guide covers complex automation scenarios that integrate Ranorex with external systems, databases, APIs, and CI/CD pipelines. These examples demonstrate enterprise-level automation patterns used in production environments.
🎯 Integration Categories
| Category | Description | Use Cases |
|---|---|---|
| Database Integration | SQL Server, MySQL, Oracle connectivity | Data-driven testing, Test data setup |
| API Validation | REST API integration with UI testing | End-to-end validation, Microservices testing |
| CI/CD Patterns | Jenkins, Azure DevOps, GitHub Actions | Automated test execution, Result reporting |
| Custom Reporting | External reporting systems | Dashboard integration, Business intelligence |
| Cloud Integration | AWS, Azure, GCP services | Cloud-native testing, Scalability |
Database Integration
Advanced Database-Driven Testing
Comprehensive database integration with multiple data sources and advanced query patterns.
using Ranorex;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
public class DatabaseTestFramework
{
private readonly string _connectionString;
private readonly Dictionary<string, object> _testContext;
public DatabaseTestFramework(string connectionString)
{
_connectionString = connectionString;
_testContext = new Dictionary<string, object>();
}
/// <summary>
/// Executes parameterized data-driven tests from database
/// </summary>
public void ExecuteDataDrivenTests(string query, Action<DataRow> testAction)
{
Host.Initialize();
try
{
var testData = ExecuteQuery(query);
int testCount = 0;
int passCount = 0;
foreach (DataRow row in testData.Rows)
{
testCount++;
string testId = row.GetValue("TestId", $"Test_{testCount}");
try
{
Report.Info("Database Test", $"Executing test: {testId}");
// Set up test context
SetupTestContext(row);
// Execute the test action
testAction(row);
passCount++;
Report.Success("Database Test", $"Test {testId} passed");
// Update test results in database
UpdateTestResult(testId, "PASS", null);
}
catch (Exception ex)
{
Report.Failure("Database Test", $"Test {testId} failed: {ex.Message}");
ScreenshotHelper.CaptureOnFailure($"DatabaseTest_{testId}", ex);
// Update test results in database
UpdateTestResult(testId, "FAIL", ex.Message);
}
finally
{
CleanupTestContext();
}
}
Report.Info("Database Test Summary",
$"Executed {testCount} tests. Passed: {passCount}, Failed: {testCount - passCount}");
}
finally
{
Host.Shutdown();
}
}
/// <summary>
/// Sets up test data and environment from database row
/// </summary>
private void SetupTestContext(DataRow row)
{
_testContext.Clear();
foreach (DataColumn column in row.Table.Columns)
{
_testContext[column.ColumnName] = row[column];
}
// Setup any required test data
if (_testContext.ContainsKey("SetupScript"))
{
ExecuteNonQuery(_testContext["SetupScript"].ToString());
}
}
/// <summary>
/// Cleans up test context and data
/// </summary>
private void CleanupTestContext()
{
if (_testContext.ContainsKey("CleanupScript"))
{
ExecuteNonQuery(_testContext["CleanupScript"].ToString());
}
}
/// <summary>
/// Executes a SQL query and returns DataTable
/// </summary>
public DataTable ExecuteQuery(string query, params SqlParameter[] parameters)
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
using (var command = new SqlCommand(query, connection))
{
if (parameters != null)
{
command.Parameters.AddRange(parameters);
}
using (var adapter = new SqlDataAdapter(command))
{
var dataTable = new DataTable();
adapter.Fill(dataTable);
return dataTable;
}
}
}
}
/// <summary>
/// Executes a non-query SQL command
/// </summary>
public int ExecuteNonQuery(string query, params SqlParameter[] parameters)
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
using (var command = new SqlCommand(query, connection))
{
if (parameters != null)
{
command.Parameters.AddRange(parameters);
}
return command.ExecuteNonQuery();
}
}
}
/// <summary>
/// Updates test result in database
/// </summary>
private void UpdateTestResult(string testId, string status, string errorMessage)
{
try
{
var query = @"
UPDATE TestResults
SET Status = @Status,
ErrorMessage = @ErrorMessage,
ExecutionTime = @ExecutionTime,
LastRun = @LastRun
WHERE TestId = @TestId";
var parameters = new[]
{
new SqlParameter("@TestId", testId),
new SqlParameter("@Status", status),
new SqlParameter("@ErrorMessage", errorMessage ?? (object)DBNull.Value),
new SqlParameter("@ExecutionTime", DateTime.Now),
new SqlParameter("@LastRun", DateTime.Now)
};
ExecuteNonQuery(query, parameters);
}
catch (Exception ex)
{
Report.Warn("Database", $"Failed to update test result: {ex.Message}");
}
}
/// <summary>
/// Gets test configuration from database
/// </summary>
public T GetTestConfig<T>(string configKey, T defaultValue = default(T))
{
try
{
var query = "SELECT ConfigValue FROM TestConfiguration WHERE ConfigKey = @ConfigKey";
var parameter = new SqlParameter("@ConfigKey", configKey);
var result = ExecuteQuery(query, parameter);
if (result.Rows.Count > 0)
{
return (T)Convert.ChangeType(result.Rows[0]["ConfigValue"], typeof(T));
}
}
catch (Exception ex)
{
Report.Warn("Database Config", $"Failed to get config '{configKey}': {ex.Message}");
}
return defaultValue;
}
}
// Extension method for DataRow
public static class DataRowExtensions
{
public static T GetValue<T>(this DataRow row, string columnName, T defaultValue = default(T))
{
if (row.Table.Columns.Contains(columnName) && row[columnName] != DBNull.Value)
{
return (T)Convert.ChangeType(row[columnName], typeof(T));
}
return defaultValue;
}
}
// Usage example
public class DatabaseDrivenLoginTest
{
public void ExecuteLoginTests()
{
var connectionString = ConfigurationManager.ConnectionStrings["TestDB"].ConnectionString;
var dbFramework = new DatabaseTestFramework(connectionString);
var query = @"
SELECT TestId, Username, Password, ExpectedResult, SetupScript, CleanupScript
FROM LoginTestCases
WHERE IsActive = 1
ORDER BY Priority";
dbFramework.ExecuteDataDrivenTests(query, (row) =>
{
var username = row.GetValue<string>("Username");
var password = row.GetValue<string>("Password");
var expectedResult = row.GetValue<string>("ExpectedResult");
// Execute the login test
var repo = MyAppRepository.Instance;
repo.LoginDialog.UsernameField.PressKeys(username);
repo.LoginDialog.PasswordField.PressKeys(password);
repo.LoginDialog.LoginButton.Click();
// Validate based on expected result
if (expectedResult == "SUCCESS")
{
repo.MainWindow.Self.WaitForExists(10000);
if (!repo.MainWindow.Self.Exists)
{
throw new AssertionException("Login should have succeeded but failed");
}
}
else
{
repo.LoginDialog.ErrorMessage.WaitForExists(5000);
if (!repo.LoginDialog.ErrorMessage.Exists)
{
throw new AssertionException("Login should have failed but succeeded");
}
}
});
}
}
API Validation
REST API Integration with UI Testing
Complete API testing framework integrated with UI automation.
using Ranorex;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
public class ApiTestFramework
{
private readonly HttpClient _httpClient;
private readonly string _baseApiUrl;
private readonly Dictionary<string, string> _headers;
public ApiTestFramework(string baseApiUrl)
{
_baseApiUrl = baseApiUrl;
_httpClient = new HttpClient();
_headers = new Dictionary<string, string>();
// Set common headers
_httpClient.DefaultRequestHeaders.Add("User-Agent", "RanorexAutomation/1.0");
}
/// <summary>
/// Adds authentication header
/// </summary>
public void SetAuthentication(string token, string scheme = "Bearer")
{
_httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue(scheme, token);
Report.Info("API Auth", "Authentication header set");
}
/// <summary>
/// Performs API request and returns response
/// </summary>
public async Task<ApiResponse<T>> SendRequestAsync<T>(string endpoint, HttpMethod method,
object requestBody = null, Dictionary<string, string> queryParams = null)
{
try
{
var url = BuildUrl(endpoint, queryParams);
var request = new HttpRequestMessage(method, url);
if (requestBody != null)
{
var json = JsonConvert.SerializeObject(requestBody);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
Report.Info("API Request", $"{method} {url}");
var response = await _httpClient.SendAsync(request);
var responseBody = await response.Content.ReadAsStringAsync();
var apiResponse = new ApiResponse<T>
{
StatusCode = (int)response.StatusCode,
IsSuccess = response.IsSuccessStatusCode,
Headers = response.Headers.ToDictionary(h => h.Key, h => string.Join(", ", h.Value)),
RawResponse = responseBody
};
if (response.IsSuccessStatusCode && !string.IsNullOrEmpty(responseBody))
{
try
{
apiResponse.Data = JsonConvert.DeserializeObject<T>(responseBody);
}
catch (JsonException ex)
{
Report.Warn("API", $"Failed to deserialize response: {ex.Message}");
}
}
Report.Info("API Response", $"Status: {apiResponse.StatusCode}, Success: {apiResponse.IsSuccess}");
return apiResponse;
}
catch (Exception ex)
{
Report.Error("API", $"Request failed: {ex.Message}");
throw;
}
}
private string BuildUrl(string endpoint, Dictionary<string, string> queryParams)
{
var url = $"{_baseApiUrl.TrimEnd('/')}/{endpoint.TrimStart('/')}";
if (queryParams != null && queryParams.Count > 0)
{
var queryString = string.Join("&",
queryParams.Select(kvp => $"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value)}"));
url += $"?{queryString}";
}
return url;
}
public void Dispose()
{
_httpClient?.Dispose();
}
}
public class ApiResponse<T>
{
public int StatusCode { get; set; }
public bool IsSuccess { get; set; }
public T Data { get; set; }
public string RawResponse { get; set; }
public Dictionary<string, string> Headers { get; set; }
}
// Complete API + UI test example
public class ApiUiIntegrationTest
{
private ApiTestFramework _apiFramework;
private MyAppRepository _repo;
public async Task ExecuteEndToEndTest()
{
Host.Initialize();
try
{
_apiFramework = new ApiTestFramework("https://api.example.com");
_repo = MyAppRepository.Instance;
// Step 1: Create user via API
var newUser = await CreateUserViaApi();
// Step 2: Verify user exists via API
await VerifyUserViaApi(newUser.UserId);
// Step 3: Test UI login with new user
await TestUiLoginWithNewUser(newUser);
// Step 4: Perform UI operations and verify via API
await PerformUiOperationsAndVerify(newUser);
// Step 5: Clean up via API
await DeleteUserViaApi(newUser.UserId);
Report.Success("Integration Test", "End-to-end API + UI test completed successfully");
}
catch (Exception ex)
{
Report.Failure("Integration Test", $"End-to-end test failed: {ex.Message}");
ScreenshotHelper.CaptureOnFailure("ApiUiIntegration", ex);
throw;
}
finally
{
_apiFramework?.Dispose();
Host.Shutdown();
}
}
private async Task<UserModel> CreateUserViaApi()
{
var userData = new
{
Username = $"testuser_{DateTime.Now:yyyyMMddHHmmss}",
Email = $"test_{DateTime.Now:yyyyMMddHHmmss}@example.com",
Password = "TestPassword123!",
FirstName = "Test",
LastName = "User"
};
var response = await _apiFramework.SendRequestAsync<UserModel>(
"users", HttpMethod.Post, userData);
if (!response.IsSuccess)
{
throw new Exception($"Failed to create user via API: {response.StatusCode}");
}
Report.Success("API", $"Created user: {response.Data.Username}");
return response.Data;
}
private async Task VerifyUserViaApi(string userId)
{
var response = await _apiFramework.SendRequestAsync<UserModel>(
$"users/{userId}", HttpMethod.Get);
if (!response.IsSuccess)
{
throw new Exception($"Failed to verify user via API: {response.StatusCode}");
}
Report.Success("API", $"Verified user exists: {response.Data.Username}");
}
private async Task TestUiLoginWithNewUser(UserModel user)
{
// Open application
_repo.LoginDialog.Self.WaitForExists(10000);
// Perform login
_repo.LoginDialog.UsernameField.PressKeys(user.Username);
_repo.LoginDialog.PasswordField.PressKeys("TestPassword123!");
_repo.LoginDialog.LoginButton.Click();
// Verify successful login
_repo.MainWindow.Self.WaitForExists(10000);
if (!_repo.MainWindow.Self.Exists)
{
throw new Exception("UI login failed - main window not found");
}
// Verify user info displayed
var displayedName = _repo.MainWindow.UserDisplayName.Text;
if (!displayedName.Contains(user.FirstName))
{
throw new Exception($"Expected user name '{user.FirstName}' not found in UI");
}
Report.Success("UI", "Login successful and user info verified");
}
private async Task PerformUiOperationsAndVerify(UserModel user)
{
// Perform some UI operations (e.g., update profile)
_repo.MainWindow.ProfileMenu.Click();
_repo.ProfileDialog.LastNameField.PressKeys("_Updated");
_repo.ProfileDialog.SaveButton.Click();
// Wait for save confirmation
WaitHelper.WaitForElement("//*[contains(text(), 'Profile updated')]",
timeout: TimeSpan.FromSeconds(10), description: "profile update confirmation");
// Verify changes via API
var response = await _apiFramework.SendRequestAsync<UserModel>(
$"users/{user.UserId}", HttpMethod.Get);
if (!response.Data.LastName.EndsWith("_Updated"))
{
throw new Exception("Profile update not reflected in API data");
}
Report.Success("Integration", "UI changes verified via API");
}
private async Task DeleteUserViaApi(string userId)
{
var response = await _apiFramework.SendRequestAsync<object>(
$"users/{userId}", HttpMethod.Delete);
if (!response.IsSuccess)
{
Report.Warn("API", $"Failed to delete user: {response.StatusCode}");
}
else
{
Report.Success("API", "User deleted successfully");
}
}
}
public class UserModel
{
public string UserId { get; set; }
public string Username { get; set; }
public string Email { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime CreatedDate { get; set; }
}
CI/CD Patterns
Jenkins Integration
Complete Jenkins pipeline integration with result reporting.
using Ranorex;
using System;
using System.IO;
using System.Xml.Linq;
public class JenkinsIntegration
{
private readonly string _buildNumber;
private readonly string _jobName;
private readonly string _workspace;
public JenkinsIntegration()
{
_buildNumber = Environment.GetEnvironmentVariable("BUILD_NUMBER") ?? "local";
_jobName = Environment.GetEnvironmentVariable("JOB_NAME") ?? "manual-run";
_workspace = Environment.GetEnvironmentVariable("WORKSPACE") ?? Environment.CurrentDirectory;
}
/// <summary>
/// Configures Ranorex for CI environment
/// </summary>
public void ConfigureForCI()
{
// Set report configuration for CI
var reportPath = Path.Combine(_workspace, "TestResults", $"Report_{_buildNumber}.rxlog");
// Ensure directory exists
Directory.CreateDirectory(Path.GetDirectoryName(reportPath));
// Configure Ranorex reporting
TestReport.Setup(ReportLevel.Debug, reportPath, true);
Report.Info("CI Setup", $"Build: {_buildNumber}, Job: {_jobName}");
Report.Info("CI Setup", $"Report path: {reportPath}");
// Set timeouts for CI environment
Host.Current.SearchTimeout = TimeSpan.FromSeconds(30);
// Disable UI interactions that might interfere with CI
Mouse.DefaultMoveTime = 0;
Keyboard.DefaultKeyPressTime = 0;
}
/// <summary>
/// Generates JUnit XML report for Jenkins
/// </summary>
public void GenerateJUnitReport(List<TestResult> testResults)
{
try
{
var junitPath = Path.Combine(_workspace, "TestResults", $"junit-{_buildNumber}.xml");
var testSuite = new XElement("testsuite",
new XAttribute("name", _jobName),
new XAttribute("tests", testResults.Count),
new XAttribute("failures", testResults.Count(r => r.Status == "FAIL")),
new XAttribute("errors", testResults.Count(r => r.Status == "ERROR")),
new XAttribute("time", testResults.Sum(r => r.Duration.TotalSeconds)),
new XAttribute("timestamp", DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss"))
);
foreach (var result in testResults)
{
var testCase = new XElement("testcase",
new XAttribute("name", result.TestName),
new XAttribute("classname", result.TestClass),
new XAttribute("time", result.Duration.TotalSeconds)
);
if (result.Status == "FAIL")
{
testCase.Add(new XElement("failure",
new XAttribute("message", result.ErrorMessage ?? "Test failed"),
result.ErrorDetails));
}
else if (result.Status == "ERROR")
{
testCase.Add(new XElement("error",
new XAttribute("message", result.ErrorMessage ?? "Test error"),
result.ErrorDetails));
}
testSuite.Add(testCase);
}
var document = new XDocument(
new XDeclaration("1.0", "UTF-8", null),
testSuite
);
document.Save(junitPath);
Report.Success("CI Report", $"JUnit report generated: {junitPath}");
}
catch (Exception ex)
{
Report.Error("CI Report", $"Failed to generate JUnit report: {ex.Message}");
}
}
/// <summary>
/// Publishes test artifacts to Jenkins
/// </summary>
public void PublishArtifacts()
{
try
{
var artifactsDir = Path.Combine(_workspace, "TestArtifacts");
Directory.CreateDirectory(artifactsDir);
// Copy screenshots
var screenshotDir = Path.Combine(Environment.CurrentDirectory, "Screenshots");
if (Directory.Exists(screenshotDir))
{
var targetScreenshotDir = Path.Combine(artifactsDir, "Screenshots");
Directory.CreateDirectory(targetScreenshotDir);
foreach (var file in Directory.GetFiles(screenshotDir, "*.png"))
{
File.Copy(file, Path.Combine(targetScreenshotDir, Path.GetFileName(file)), true);
}
Report.Info("CI Artifacts", "Screenshots copied to artifacts directory");
}
// Copy Ranorex reports
var reportFiles = Directory.GetFiles(_workspace, "*.rxlog", SearchOption.AllDirectories);
foreach (var report in reportFiles)
{
File.Copy(report, Path.Combine(artifactsDir, Path.GetFileName(report)), true);
}
Report.Success("CI Artifacts", $"Artifacts published to: {artifactsDir}");
}
catch (Exception ex)
{
Report.Error("CI Artifacts", $"Failed to publish artifacts: {ex.Message}");
}
}
/// <summary>
/// Sets Jenkins build status based on test results
/// </summary>
public void SetBuildStatus(List<TestResult> testResults)
{
var totalTests = testResults.Count;
var failedTests = testResults.Count(r => r.Status != "PASS");
var passRate = totalTests > 0 ? ((totalTests - failedTests) * 100.0 / totalTests) : 0;
Console.WriteLine($"##teamcity[buildStatisticValue key='TotalTests' value='{totalTests}']");
Console.WriteLine($"##teamcity[buildStatisticValue key='PassedTests' value='{totalTests - failedTests}']");
Console.WriteLine($"##teamcity[buildStatisticValue key='FailedTests' value='{failedTests}']");
Console.WriteLine($"##teamcity[buildStatisticValue key='PassRate' value='{passRate:F2}']");
if (failedTests > 0)
{
Environment.ExitCode = 1;
Report.Error("CI Status", $"Build failed: {failedTests} out of {totalTests} tests failed");
}
else
{
Environment.ExitCode = 0;
Report.Success("CI Status", $"Build passed: All {totalTests} tests passed");
}
}
}
// Test result model
public class TestResult
{
public string TestName { get; set; }
public string TestClass { get; set; }
public string Status { get; set; } // PASS, FAIL, ERROR
public string ErrorMessage { get; set; }
public string ErrorDetails { get; set; }
public TimeSpan Duration { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
}
Custom Reporting
External Dashboard Integration
Integration with external reporting and dashboard systems.
using Ranorex;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
public class DashboardReporter
{
private readonly string _dashboardUrl;
private readonly string _apiKey;
private readonly HttpClient _httpClient;
public DashboardReporter(string dashboardUrl, string apiKey)
{
_dashboardUrl = dashboardUrl;
_apiKey = apiKey;
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
}
/// <summary>
/// Sends test execution results to external dashboard
/// </summary>
public async Task ReportTestExecution(TestExecutionReport report)
{
try
{
var json = JsonConvert.SerializeObject(report);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync($"{_dashboardUrl}/api/test-results", content);
if (response.IsSuccessStatusCode)
{
Report.Success("Dashboard", "Test results sent to dashboard successfully");
}
else
{
Report.Warn("Dashboard", $"Failed to send results to dashboard: {response.StatusCode}");
}
}
catch (Exception ex)
{
Report.Error("Dashboard", $"Error sending results to dashboard: {ex.Message}");
}
}
/// <summary>
/// Sends real-time test progress updates
/// </summary>
public async Task UpdateTestProgress(string testId, string status, string message = null)
{
try
{
var progressUpdate = new
{
TestId = testId,
Status = status,
Message = message,
Timestamp = DateTime.UtcNow
};
var json = JsonConvert.SerializeObject(progressUpdate);
var content = new StringContent(json, Encoding.UTF8, "application/json");
await _httpClient.PostAsync($"{_dashboardUrl}/api/test-progress", content);
}
catch (Exception ex)
{
Report.Debug("Dashboard", $"Failed to update progress: {ex.Message}");
}
}
}
public class TestExecutionReport
{
public string TestSuiteId { get; set; }
public string Environment { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public List<TestResult> Results { get; set; }
public Dictionary<string, object> Metadata { get; set; }
}
Cloud Integration
AWS Integration Example
using Amazon.S3;
using Amazon.S3.Model;
using Ranorex;
using System;
using System.IO;
using System.Threading.Tasks;
public class CloudIntegration
{
private readonly IAmazonS3 _s3Client;
private readonly string _bucketName;
public CloudIntegration(string bucketName)
{
_s3Client = new AmazonS3Client();
_bucketName = bucketName;
}
/// <summary>
/// Uploads test artifacts to S3
/// </summary>
public async Task UploadTestArtifacts(string localPath, string testRunId)
{
try
{
var files = Directory.GetFiles(localPath, "*", SearchOption.AllDirectories);
foreach (var file in files)
{
var key = $"test-artifacts/{testRunId}/{Path.GetFileName(file)}";
var request = new PutObjectRequest
{
BucketName = _bucketName,
Key = key,
FilePath = file,
ContentType = GetContentType(file)
};
await _s3Client.PutObjectAsync(request);
Report.Info("Cloud Upload", $"Uploaded: {Path.GetFileName(file)}");
}
}
catch (Exception ex)
{
Report.Error("Cloud Upload", $"Failed to upload artifacts: {ex.Message}");
}
}
private string GetContentType(string filePath)
{
var extension = Path.GetExtension(filePath).ToLower();
return extension switch
{
".png" => "image/png",
".jpg" => "image/jpeg",
".html" => "text/html",
".xml" => "application/xml",
".json" => "application/json",
_ => "application/octet-stream"
};
}
}
Summary
These advanced integration examples demonstrate:
- Database-Driven Testing - Enterprise-grade data management
- API + UI Integration - End-to-end validation patterns
- CI/CD Integration - Automated pipeline execution
- Custom Reporting - External dashboard integration
- Cloud Integration - Scalable artifact management
For implementation guidance:
- Start Simple: Begin with basic patterns and expand
- Error Handling: Implement comprehensive error recovery
- Configuration: Use external configuration for flexibility
- Monitoring: Add logging and progress tracking
- Security: Implement proper authentication and secrets management
These patterns form the foundation for enterprise-scale test automation frameworks that integrate seamlessly with your development and deployment pipelines.