Skip to content
This repository was archived by the owner on Oct 10, 2025. It is now read-only.

Commit 42c52c4

Browse files
committed
refactor: [#14] move general utility functions from test-e2e.sh to shell-utils.sh
- Extract general helper functions to scripts/shell-utils.sh for project-wide reuse: * test_http_endpoint() - HTTP endpoint testing with content validation * retry_with_timeout() - Configurable retry logic with custom parameters * time_operation() - Operation timing with automatic duration logging - Keep project-specific functions in tests/test-e2e.sh: * get_vm_ip() - Specific to torrust-tracker-demo VM name * ssh_to_vm() - Specific to torrust user and VM configuration - Benefits achieved: * Reduced code duplication across the project * Centralized common patterns for better maintainability * Standardized error handling and logging * Better separation of concerns between test logic and utilities - Quality assurance: * All code passes ShellCheck linting * End-to-end test passes (2m 42s execution time) * Functions maintain backward compatibility * Enhanced documentation and usage examples This refactoring eliminates duplicate code patterns while creating reusable utilities that can benefit other scripts in the infrastructure and application directories.
1 parent 71e04ea commit 42c52c4

File tree

2 files changed

+133
-56
lines changed

2 files changed

+133
-56
lines changed

scripts/shell-utils.sh

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@
1414
# log_success "Operation completed successfully"
1515
# log_warning "This is a warning"
1616
# log_error "This is an error"
17+
#
18+
# # Use HTTP testing:
19+
# result=$(test_http_endpoint "http://example.com" "expected content")
20+
# if [[ "$result" == "success" ]]; then echo "Endpoint working"; fi
21+
#
22+
# # Use retry logic:
23+
# retry_with_timeout "Testing connection" 5 2 "ping -c1 example.com >/dev/null"
24+
#
25+
# # Time operations:
26+
# time_operation "Deployment" "make deploy"
1727

1828
# Shared shell utilities - can be sourced multiple times safely
1929
export SHELL_UTILS_LOADED=1
@@ -212,15 +222,31 @@ ${BLUE}ENVIRONMENT VARIABLES:${NC}
212222
TRACE Set to 'true' to enable trace logging
213223
DRY_RUN Set to 'true' to show commands without executing
214224
225+
${BLUE}AVAILABLE FUNCTIONS:${NC}
226+
Logging: log_info, log_success, log_warning, log_error, log_debug, log_trace
227+
HTTP Testing: test_http_endpoint <url> [expected_content] [timeout]
228+
Retry Logic: retry_with_timeout <description> <max_attempts> <sleep_interval> <command>
229+
Timing: time_operation <operation_name> <command>
230+
Sudo Management: ensure_sudo_cached, run_with_sudo, clear_sudo_cache
231+
Utilities: command_exists, safe_cd, execute_with_log, require_env_vars
232+
215233
${BLUE}EXAMPLES:${NC}
216234
# Enable logging to file
217235
export SHELL_UTILS_LOG_FILE="/tmp/my-script.log"
218236
219237
# Enable debug mode
220238
export DEBUG=true
221239
222-
# Dry run mode
223-
export DRY_RUN=true
240+
# Test HTTP endpoint
241+
if [[ \$(test_http_endpoint "https://example.com" "success") == "success" ]]; then
242+
log_success "Endpoint is working"
243+
fi
244+
245+
# Retry with timeout
246+
retry_with_timeout "Testing connection" 5 2 "ping -c1 example.com >/dev/null"
247+
248+
# Time an operation
249+
time_operation "Deployment" "make deploy"
224250
225251
EOF
226252
}
@@ -274,3 +300,73 @@ clear_sudo_cache() {
274300
sudo -k
275301
log_debug "Sudo credentials cache cleared"
276302
}
303+
304+
# HTTP and Network Testing Functions
305+
306+
# Test HTTP endpoints with optional content validation
307+
test_http_endpoint() {
308+
local url="$1"
309+
local expected_content="$2"
310+
local timeout="${3:-5}"
311+
312+
local response
313+
response=$(curl -f -s --max-time "${timeout}" "${url}" 2>/dev/null || echo "")
314+
315+
if [[ -n "${expected_content}" ]] && echo "${response}" | grep -q "${expected_content}"; then
316+
echo "success"
317+
elif [[ -z "${expected_content}" ]] && [[ -n "${response}" ]]; then
318+
echo "success"
319+
else
320+
echo "failed"
321+
fi
322+
}
323+
324+
# Retry Logic and Timing Functions
325+
326+
# Execute a command with retry logic and configurable parameters
327+
retry_with_timeout() {
328+
local description="$1"
329+
local max_attempts="$2"
330+
local sleep_interval="$3"
331+
local test_command="$4"
332+
333+
local attempt=1
334+
while [[ ${attempt} -le ${max_attempts} ]]; do
335+
log_info "${description} (attempt ${attempt}/${max_attempts})..."
336+
337+
if eval "${test_command}"; then
338+
return 0
339+
fi
340+
341+
if [[ ${attempt} -eq ${max_attempts} ]]; then
342+
log_error "${description} failed after ${max_attempts} attempts"
343+
return 1
344+
fi
345+
346+
sleep "${sleep_interval}"
347+
((attempt++))
348+
done
349+
}
350+
351+
# Time an operation and log the duration
352+
time_operation() {
353+
local operation_name="$1"
354+
local command="$2"
355+
356+
local start_time
357+
start_time=$(date +%s)
358+
359+
if eval "${command}"; then
360+
local end_time
361+
end_time=$(date +%s)
362+
local duration=$((end_time - start_time))
363+
log_success "${operation_name} completed successfully in ${duration} seconds"
364+
return 0
365+
else
366+
local end_time
367+
end_time=$(date +%s)
368+
local duration=$((end_time - start_time))
369+
log_error "${operation_name} failed after ${duration} seconds"
370+
return 1
371+
fi
372+
}

tests/test-e2e.sh

Lines changed: 35 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,20 @@ log_section() {
3232
log "${BLUE}===============================================${NC}"
3333
}
3434

35+
# Helper function to get VM IP address from libvirt
36+
get_vm_ip() {
37+
virsh domifaddr torrust-tracker-demo 2>/dev/null | grep ipv4 | awk '{print $4}' | cut -d'/' -f1 || echo ""
38+
}
39+
40+
# Helper function for SSH connections with standard options
41+
ssh_to_vm() {
42+
local vm_ip="$1"
43+
local command="$2"
44+
local output_redirect="${3:->/dev/null 2>&1}"
45+
46+
eval ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no torrust@"${vm_ip}" "\"${command}\"" "${output_redirect}"
47+
}
48+
3549
# Track test start time
3650
TEST_START_TIME=$(date +%s)
3751

@@ -106,19 +120,11 @@ test_infrastructure_provisioning() {
106120

107121
# Provision infrastructure (Step 2.3 from guide)
108122
log_info "Provisioning infrastructure..."
109-
local start_time
110-
start_time=$(date +%s)
111-
112-
if ! make infra-apply ENVIRONMENT="${ENVIRONMENT}"; then
123+
if ! time_operation "Infrastructure provisioning" "make infra-apply ENVIRONMENT=\"${ENVIRONMENT}\""; then
113124
log_error "Infrastructure provisioning failed"
114125
return 1
115126
fi
116127

117-
local end_time
118-
end_time=$(date +%s)
119-
local duration=$((end_time - start_time))
120-
log_success "Infrastructure provisioned successfully in ${duration} seconds"
121-
122128
# Verify infrastructure (Step 2.4 from guide)
123129
log_info "Verifying infrastructure status..."
124130
if ! make infra-status ENVIRONMENT="${ENVIRONMENT}"; then
@@ -149,22 +155,14 @@ test_application_deployment() {
149155

150156
# Deploy application (Step 3.1 from guide)
151157
log_info "Deploying application using twelve-factor workflow..."
152-
local start_time
153-
start_time=$(date +%s)
154-
155-
if ! make app-deploy ENVIRONMENT="${ENVIRONMENT}"; then
158+
if ! time_operation "Application deployment" "make app-deploy ENVIRONMENT=\"${ENVIRONMENT}\""; then
156159
log_error "Application deployment failed"
157160
return 1
158161
fi
159162

160163
# Note: app-deploy includes health validation via validate_deployment function
161164
log_info "Application deployment completed with built-in health validation"
162165

163-
local end_time
164-
end_time=$(date +%s)
165-
local duration=$((end_time - start_time))
166-
log_success "Application deployed successfully in ${duration} seconds"
167-
168166
return 0
169167
}
170168

@@ -187,32 +185,21 @@ test_health_validation() {
187185

188186
# Get VM IP for direct testing
189187
local vm_ip
190-
vm_ip=$(virsh domifaddr torrust-tracker-demo 2>/dev/null | grep ipv4 | awk '{print $4}' | cut -d'/' -f1 || echo "")
188+
vm_ip=$(get_vm_ip)
191189

192190
if [[ -n "${vm_ip}" ]]; then
193191
log_info "Testing application endpoints on ${vm_ip}..."
194192

195193
# Test tracker health endpoint (may take a moment to be ready)
196-
local max_attempts=12 # 2 minutes
197-
local attempt=1
198-
while [[ ${attempt} -le ${max_attempts} ]]; do
199-
log_info "Testing health endpoint (attempt ${attempt}/${max_attempts})..."
200-
# shellcheck disable=SC2034,SC2086
201-
if curl -f -s http://"${vm_ip}"/api/health_check >/dev/null 2>&1; then
202-
log_success "Health endpoint responding"
203-
break
204-
fi
205-
if [[ ${attempt} -eq ${max_attempts} ]]; then
206-
log_warning "Health endpoint not responding after ${max_attempts} attempts"
207-
else
208-
sleep 10
209-
fi
210-
((attempt++))
211-
done
194+
if retry_with_timeout "Testing health endpoint" 12 10 "test_http_endpoint \"http://${vm_ip}/api/health_check\" '\"status\":\"Ok\"' >/dev/null"; then # DevSkim: ignore DS137138
195+
log_success "Health endpoint responding"
196+
else
197+
log_warning "Health endpoint not responding after 12 attempts"
198+
fi
212199

213200
# Test if basic services are running
214201
log_info "Checking if Docker services are running..."
215-
if ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no torrust@"${vm_ip}" "cd /home/torrust/github/torrust/torrust-tracker-demo/application && docker compose ps --services --filter status=running" 2>/dev/null | grep -q tracker; then
202+
if ssh_to_vm "${vm_ip}" "cd /home/torrust/github/torrust/torrust-tracker-demo/application && docker compose ps --services --filter status=running" 2>/dev/null | grep -q tracker; then
216203
log_success "Tracker service is running"
217204
else
218205
log_warning "Tracker service may not be running yet"
@@ -231,7 +218,7 @@ test_smoke_testing() {
231218

232219
# Get VM IP for testing
233220
local vm_ip
234-
vm_ip=$(virsh domifaddr torrust-tracker-demo 2>/dev/null | grep ipv4 | awk '{print $4}' | cut -d'/' -f1 || echo "")
221+
vm_ip=$(get_vm_ip)
235222

236223
if [[ -z "${vm_ip}" ]]; then
237224
log_error "VM IP not available - cannot run mandatory smoke tests"
@@ -245,23 +232,19 @@ test_smoke_testing() {
245232

246233
# Test 1: Health Check API (through nginx proxy on port 80)
247234
log_info "Testing health check API through nginx proxy..."
248-
local health_response
249-
health_response=$(curl -f -s http://"${vm_ip}":80/api/health_check 2>/dev/null || echo "")
250-
if echo "${health_response}" | grep -q '"status":"Ok"'; then
235+
if [[ $(test_http_endpoint "http://${vm_ip}:80/api/health_check" '"status":"Ok"') == "success" ]]; then # DevSkim: ignore DS137138
251236
log_success "✓ Health check API working"
252237
else
253-
log_error "✗ Health check API failed - Response: ${health_response}"
238+
log_error "✗ Health check API failed"
254239
((failed_tests++))
255240
fi
256241

257242
# Test 2: Statistics API (through nginx proxy on port 80)
258243
log_info "Testing statistics API through nginx proxy..."
259-
local stats_response
260-
stats_response=$(curl -f -s "http://${vm_ip}:80/api/v1/stats?token=local-dev-admin-token-12345" 2>/dev/null || echo "") # DevSkim: ignore DS137138
261-
if echo "${stats_response}" | grep -q '"torrents"'; then
244+
if [[ $(test_http_endpoint "http://${vm_ip}:80/api/v1/stats?token=local-dev-admin-token-12345" '"torrents"') == "success" ]]; then # DevSkim: ignore DS137138
262245
log_success "✓ Statistics API working"
263246
else
264-
log_error "✗ Statistics API failed - Response: ${stats_response}"
247+
log_error "✗ Statistics API failed"
265248
((failed_tests++))
266249
fi
267250

@@ -304,12 +287,10 @@ test_smoke_testing() {
304287

305288
# Test 6: Direct tracker health check (port 1212)
306289
log_info "Testing direct tracker health check on port 1212..."
307-
local direct_health
308-
direct_health=$(curl -f -s http://"${vm_ip}":1212/api/health_check 2>/dev/null || echo "")
309-
if echo "${direct_health}" | grep -q '"status":"Ok"'; then
290+
if [[ $(test_http_endpoint "http://${vm_ip}:1212/api/health_check" '"status":"Ok"') == "success" ]]; then # DevSkim: ignore DS137138
310291
log_success "✓ Direct tracker health check working"
311292
else
312-
log_error "✗ Direct tracker health check failed - Response: ${direct_health}"
293+
log_error "✗ Direct tracker health check failed"
313294
((failed_tests++))
314295
fi
315296

@@ -399,7 +380,7 @@ wait_for_vm_ip() {
399380
fi
400381

401382
# Also check libvirt directly as fallback
402-
vm_ip=$(virsh domifaddr torrust-tracker-demo 2>/dev/null | grep ipv4 | awk '{print $4}' | cut -d'/' -f1 || echo "")
383+
vm_ip=$(get_vm_ip)
403384
if [[ -n "${vm_ip}" ]]; then
404385
log_success "VM IP assigned (via libvirt): ${vm_ip}"
405386
# Refresh terraform state to sync with actual VM state
@@ -427,7 +408,7 @@ wait_for_cloud_init_to_finish() {
427408
local vm_ip=""
428409

429410
# First get the VM IP
430-
vm_ip=$(virsh domifaddr torrust-tracker-demo 2>/dev/null | grep ipv4 | awk '{print $4}' | cut -d'/' -f1 || echo "")
411+
vm_ip=$(get_vm_ip)
431412
if [[ -z "${vm_ip}" ]]; then
432413
log_error "VM IP not available - cannot check readiness"
433414
return 1
@@ -439,7 +420,7 @@ wait_for_cloud_init_to_finish() {
439420
log_info "Checking cloud-init status (attempt ${attempt}/${max_attempts})..."
440421

441422
# Check if SSH is available
442-
if ! ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no torrust@"${vm_ip}" "echo 'SSH OK'" >/dev/null 2>&1; then
423+
if ! ssh_to_vm "${vm_ip}" "echo 'SSH OK'"; then
443424
log_info "SSH not ready yet, waiting 10 seconds..."
444425
sleep 10
445426
((attempt++))
@@ -448,13 +429,13 @@ wait_for_cloud_init_to_finish() {
448429

449430
# Check if cloud-init has finished
450431
local cloud_init_status
451-
cloud_init_status=$(ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no torrust@"${vm_ip}" "cloud-init status" 2>/dev/null || echo "unknown")
432+
cloud_init_status=$(ssh_to_vm "${vm_ip}" "cloud-init status" "2>/dev/null" || echo "unknown")
452433

453434
if [[ "${cloud_init_status}" == *"done"* ]]; then
454435
log_success "Cloud-init completed successfully"
455436

456437
# Check if Docker is available and working
457-
if ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no torrust@"${vm_ip}" "docker --version && docker compose version" >/dev/null 2>&1; then
438+
if ssh_to_vm "${vm_ip}" "docker --version && docker compose version"; then
458439
log_success "Docker is ready and available"
459440
log_success "VM is ready for application deployment"
460441
return 0

0 commit comments

Comments
 (0)