430 lines
12 KiB
Plaintext
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)"
|
||
|
|
}
|
||
|
|
}
|