2025-10-07 11:12:02 +01:00

430 lines
12 KiB
Plaintext

# Integration Test Helpers
# Common utilities for integration testing
use std log
# Test configuration
export def load-test-config [] {
let config_path = $"($env.PWD)/provisioning/tests/integration/test_config.yaml"
if not ($config_path | path exists) {
error make {
msg: "Test configuration not found"
label: {
text: $"Config file not found: ($config_path)"
span: (metadata $config_path).span
}
}
}
open $config_path
}
# Assertion helpers
export def assert-eq [actual: any, expected: any, message: string = ""] {
if $actual != $expected {
let error_msg = if ($message | is-empty) {
$"Assertion failed: expected ($expected), got ($actual)"
} else {
$"($message): expected ($expected), got ($actual)"
}
error make {
msg: $error_msg
label: {
text: "Assertion failed"
span: (metadata $actual).span
}
}
}
}
export def assert-true [condition: bool, message: string = ""] {
if not $condition {
let error_msg = if ($message | is-empty) {
"Assertion failed: expected true, got false"
} else {
$"($message): expected true, got false"
}
error make {
msg: $error_msg
label: {
text: "Assertion failed"
span: (metadata $condition).span
}
}
}
}
export def assert-false [condition: bool, message: string = ""] {
if $condition {
let error_msg = if ($message | is-empty) {
"Assertion failed: expected false, got true"
} else {
$"($message): expected false, got true"
}
error make {
msg: $error_msg
label: {
text: "Assertion failed"
span: (metadata $condition).span
}
}
}
}
export def assert-contains [haystack: list, needle: any, message: string = ""] {
if not ($needle in $haystack) {
let error_msg = if ($message | is-empty) {
$"Assertion failed: ($haystack) does not contain ($needle)"
} else {
$"($message): ($haystack) does not contain ($needle)"
}
error make {
msg: $error_msg
label: {
text: "Assertion failed"
span: (metadata $haystack).span
}
}
}
}
export def assert-not-contains [haystack: list, needle: any, message: string = ""] {
if $needle in $haystack {
let error_msg = if ($message | is-empty) {
$"Assertion failed: ($haystack) contains ($needle)"
} else {
$"($message): ($haystack) contains ($needle)"
}
error make {
msg: $error_msg
label: {
text: "Assertion failed"
span: (metadata $haystack).span
}
}
}
}
export def assert-not-empty [value: any, message: string = ""] {
let is_empty = match ($value | describe) {
"string" => { ($value | str length) == 0 }
"list" => { ($value | length) == 0 }
"record" => { ($value | columns | length) == 0 }
_ => { false }
}
if $is_empty {
let error_msg = if ($message | is-empty) {
"Assertion failed: value is empty"
} else {
$"($message): value is empty"
}
error make {
msg: $error_msg
label: {
text: "Assertion failed"
span: (metadata $value).span
}
}
}
}
export def assert-http-success [response: record, message: string = ""] {
let status = $response.status
if $status < 200 or $status >= 300 {
let error_msg = if ($message | is-empty) {
$"HTTP request failed with status ($status)"
} else {
$"($message): HTTP request failed with status ($status)"
}
error make {
msg: $error_msg
label: {
text: "HTTP assertion failed"
span: (metadata $response).span
}
}
}
}
# Test fixture helpers
export def create-test-workspace [name: string, config: record] {
let test_config = (load-test-config)
let workspace_path = $"($test_config.test_workspace.path)-($name)"
# Create workspace directory structure
mkdir $workspace_path
mkdir $"($workspace_path)/config"
mkdir $"($workspace_path)/infra"
mkdir $"($workspace_path)/extensions"
mkdir $"($workspace_path)/runtime"
# Write workspace config
let workspace_config = {
workspace: {
name: $name
version: "1.0.0"
created: (date now | format date "%Y-%m-%d")
}
settings: $config
}
$workspace_config | save -f $"($workspace_path)/config/provisioning.yaml"
{
name: $name
path: $workspace_path
config: $workspace_config
}
}
export def cleanup-test-workspace [workspace: record] {
if ($workspace.path | path exists) {
rm -rf $workspace.path
log info $"Cleaned up test workspace: ($workspace.name)"
}
}
export def create-test-server [
name: string
provider: string = "local"
config: record = {}
] {
let test_config = (load-test-config)
# Prepare server configuration
let server_config = {
hostname: $name
provider: $provider
cores: ($config.cores? | default 2)
memory: ($config.memory? | default 4096)
storage: ($config.storage? | default 50)
zone: ($config.zone? | default "local")
}
# Call orchestrator API to create server
let orchestrator_url = $"http://($test_config.services.orchestrator.host):($test_config.services.orchestrator.port)"
let response = (http post $"($orchestrator_url)/workflows/servers/create" {
server: $server_config
check: true
})
assert-http-success $response "Server creation failed"
$response.body
}
export def delete-test-server [server_id: string] {
let test_config = (load-test-config)
let orchestrator_url = $"http://($test_config.services.orchestrator.host):($test_config.services.orchestrator.port)"
let response = (http delete $"($orchestrator_url)/servers/($server_id)")
assert-http-success $response "Server deletion failed"
$response.body
}
# Retry helper for flaky operations
export def with-retry [
--max-attempts: int = 3
--delay: int = 5
--backoff-multiplier: float = 2.0
operation: closure
] {
mut attempts = 0
mut current_delay = $delay
mut last_error = null
while $attempts < $max_attempts {
try {
return (do $operation)
} catch { |err|
$attempts = $attempts + 1
$last_error = $err
if $attempts < $max_attempts {
log warning $"Attempt ($attempts) failed: ($err.msg). Retrying in ($current_delay)s..."
sleep ($current_delay * 1sec)
$current_delay = ($current_delay * $backoff_multiplier | into int)
}
}
}
error make {
msg: $"Operation failed after ($max_attempts) attempts: ($last_error.msg)"
label: {
text: "Retry exhausted"
span: (metadata $operation).span
}
}
}
# Wait for condition with timeout
export def wait-for-condition [
--timeout: int = 60
--interval: int = 5
condition: closure
description: string = "condition"
] {
let start_time = (date now)
let timeout_duration = ($timeout * 1sec)
while true {
try {
let result = (do $condition)
if $result {
return true
}
} catch { |err|
# Ignore errors, keep waiting
}
let elapsed = ((date now) - $start_time)
if $elapsed > $timeout_duration {
error make {
msg: $"Timeout waiting for ($description) after ($timeout)s"
label: {
text: "Timeout"
span: (metadata $condition).span
}
}
}
sleep ($interval * 1sec)
}
}
# Service health check helpers
export def check-service-health [service_name: string] {
let test_config = (load-test-config)
let service_config = match $service_name {
"orchestrator" => { $test_config.services.orchestrator }
"coredns" => { $test_config.services.coredns }
"gitea" => { $test_config.services.gitea }
"postgres" => { $test_config.services.postgres }
"prometheus" => { $test_config.services.prometheus }
"grafana" => { $test_config.services.grafana }
_ => {
error make {
msg: $"Unknown service: ($service_name)"
}
}
}
match $service_name {
"orchestrator" => {
let url = $"http://($service_config.host):($service_config.port)($service_config.health_endpoint)"
try {
let response = (http get $url)
$response.status == 200
} catch {
false
}
}
"coredns" => {
# Check DNS resolution
try {
dig @$service_config.host test.local | complete | get exit_code | $in == 0
} catch {
false
}
}
"gitea" => {
let url = $"http://($service_config.host):($service_config.port)/api/v1/version"
try {
let response = (http get $url)
$response.status == 200
} catch {
false
}
}
"postgres" => {
try {
psql -h $service_config.host -p $service_config.port -U $service_config.username -d $service_config.database -c "SELECT 1" | complete | get exit_code | $in == 0
} catch {
false
}
}
_ => {
let url = $"http://($service_config.host):($service_config.port)/"
try {
let response = (http get $url)
$response.status == 200
} catch {
false
}
}
}
}
export def wait-for-service [service_name: string, timeout: int = 60] {
wait-for-condition --timeout $timeout --interval 5 {
check-service-health $service_name
} $"service ($service_name) to be healthy"
}
# Test result tracking
export def create-test-result [
test_name: string
status: string # "passed", "failed", "skipped"
duration_ms: int
error_message: string = ""
] {
{
test_name: $test_name
status: $status
duration_ms: $duration_ms
error_message: $error_message
timestamp: (date now | format date "%Y-%m-%dT%H:%M:%S%.3fZ")
}
}
# Test runner helpers
export def run-test [
test_name: string
test_fn: closure
] {
log info $"Running test: ($test_name)"
let start_time = (date now)
try {
do $test_fn
let duration = ((date now) - $start_time | into int) / 1000000
log info $"✓ Test passed: ($test_name) \(($duration)ms\)"
create-test-result $test_name "passed" $duration
} catch { |err|
let duration = ((date now) - $start_time | into int) / 1000000
log error $"✗ Test failed: ($test_name) - ($err.msg)"
create-test-result $test_name "failed" $duration $err.msg
}
}
# Cleanup helpers
export def cleanup-on-exit [cleanup_fn: closure] {
# Register cleanup function to run on exit
# Note: Nushell doesn't have built-in exit hooks, so this is a best-effort approach
try {
do $cleanup_fn
} catch { |err|
log warning $"Cleanup failed: ($err.msg)"
}
}