mirror of
https://github.com/mod-playerbots/azerothcore-wotlk.git
synced 2026-01-13 01:08:35 +00:00
Merge branch 'master' into Playerbot-updated
This commit is contained in:
267
apps/bash_shared/menu_system.sh
Normal file
267
apps/bash_shared/menu_system.sh
Normal file
@@ -0,0 +1,267 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# =============================================================================
|
||||
# AzerothCore Menu System Library
|
||||
# =============================================================================
|
||||
# This library provides a unified menu system for AzerothCore scripts.
|
||||
# It supports ordered menu definitions, short commands, numeric selection,
|
||||
# and proper argument handling.
|
||||
#
|
||||
# Features:
|
||||
# - Single source of truth for menu definitions
|
||||
# - Automatic ID assignment (1, 2, 3...)
|
||||
# - Short command aliases (c, i, q, etc.)
|
||||
# - Interactive mode: numbers + long/short commands
|
||||
# - Direct mode: only long/short commands (no numbers)
|
||||
# - Proper argument forwarding
|
||||
#
|
||||
# Usage:
|
||||
# source "path/to/menu_system.sh"
|
||||
# menu_items=("command|short|description" ...)
|
||||
# menu_run "Menu Title" callback_function "${menu_items[@]}" "$@"
|
||||
# =============================================================================
|
||||
|
||||
# Global arrays for menu state (will be populated by menu_define)
|
||||
declare -a _MENU_KEYS=()
|
||||
declare -a _MENU_SHORTS=()
|
||||
declare -a _MENU_OPTIONS=()
|
||||
|
||||
# Parse menu items and populate global arrays
|
||||
# Usage: menu_define array_elements...
|
||||
function menu_define() {
|
||||
# Clear previous state
|
||||
_MENU_KEYS=()
|
||||
_MENU_SHORTS=()
|
||||
_MENU_OPTIONS=()
|
||||
|
||||
# Parse each menu item: "key|short|description"
|
||||
local item key short desc
|
||||
for item in "$@"; do
|
||||
IFS='|' read -r key short desc <<< "$item"
|
||||
_MENU_KEYS+=("$key")
|
||||
_MENU_SHORTS+=("$short")
|
||||
_MENU_OPTIONS+=("$key ($short): $desc")
|
||||
done
|
||||
}
|
||||
|
||||
# Display menu with numbered options
|
||||
# Usage: menu_display "Menu Title"
|
||||
function menu_display() {
|
||||
local title="$1"
|
||||
|
||||
echo "==== $title ===="
|
||||
for idx in "${!_MENU_OPTIONS[@]}"; do
|
||||
local num=$((idx + 1))
|
||||
printf "%2d) %s\n" "$num" "${_MENU_OPTIONS[$idx]}"
|
||||
done
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Find menu index by user input (number, long command, or short command)
|
||||
# Returns: index (0-based) or -1 if not found
|
||||
# Usage: index=$(menu_find_index "user_input")
|
||||
function menu_find_index() {
|
||||
local user_input="$1"
|
||||
|
||||
# Try numeric selection first
|
||||
if [[ "$user_input" =~ ^[0-9]+$ ]]; then
|
||||
local num=$((user_input - 1))
|
||||
if [[ $num -ge 0 && $num -lt ${#_MENU_KEYS[@]} ]]; then
|
||||
echo "$num"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Try long command name
|
||||
local idx
|
||||
for idx in "${!_MENU_KEYS[@]}"; do
|
||||
if [[ "$user_input" == "${_MENU_KEYS[$idx]}" ]]; then
|
||||
echo "$idx"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
# Try short command
|
||||
for idx in "${!_MENU_SHORTS[@]}"; do
|
||||
if [[ "$user_input" == "${_MENU_SHORTS[$idx]}" ]]; then
|
||||
echo "$idx"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
echo "-1"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Handle direct execution (command line arguments)
|
||||
# Disables numeric selection to prevent confusion with command arguments
|
||||
# Usage: menu_direct_execute callback_function "$@"
|
||||
function menu_direct_execute() {
|
||||
local callback="$1"
|
||||
shift
|
||||
local user_input="$1"
|
||||
shift
|
||||
|
||||
# Disable numeric selection in direct mode
|
||||
if [[ "$user_input" =~ ^[0-9]+$ ]]; then
|
||||
echo "Invalid option. Numeric selection is not allowed when passing arguments."
|
||||
echo "Use command name or short alias instead."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Find command and execute
|
||||
local idx
|
||||
# try-catch
|
||||
{
|
||||
idx=$(menu_find_index "$user_input")
|
||||
} ||
|
||||
{
|
||||
idx=-1
|
||||
}
|
||||
|
||||
if [[ $idx -ge 0 ]]; then
|
||||
"$callback" "${_MENU_KEYS[$idx]}" "$@"
|
||||
return $?
|
||||
else
|
||||
# Handle help requests directly
|
||||
if [[ "$user_input" == "--help" || "$user_input" == "help" || "$user_input" == "-h" ]]; then
|
||||
echo "Available commands:"
|
||||
printf '%s\n' "${_MENU_OPTIONS[@]}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Invalid option. Use --help to see available commands." >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Handle interactive menu selection
|
||||
# Usage: menu_interactive callback_function "Menu Title"
|
||||
function menu_interactive() {
|
||||
local callback="$1"
|
||||
local title="$2"
|
||||
|
||||
while true; do
|
||||
menu_display "$title"
|
||||
read -r -p "Please enter your choice: " REPLY
|
||||
|
||||
# Parse input to separate command from arguments
|
||||
local input_parts=()
|
||||
read -r -a input_parts <<< "$REPLY"
|
||||
local user_command="${input_parts[0]}"
|
||||
local user_args=("${input_parts[@]:1}")
|
||||
|
||||
# Find and execute command
|
||||
local idx
|
||||
idx=$(menu_find_index "$user_command")
|
||||
if [[ $idx -ge 0 ]]; then
|
||||
# Pass the command key and any additional arguments
|
||||
"$callback" "${_MENU_KEYS[$idx]}" "${user_args[@]}"
|
||||
local exit_code=$?
|
||||
# Exit loop if callback returns 0 (e.g., quit command)
|
||||
if [[ $exit_code -eq 0 && "${_MENU_KEYS[$idx]}" == "quit" ]]; then
|
||||
break
|
||||
fi
|
||||
else
|
||||
# Handle help request
|
||||
if [[ "$REPLY" == "--help" || "$REPLY" == "help" || "$REPLY" == "h" ]]; then
|
||||
echo "Available commands:"
|
||||
printf '%s\n' "${_MENU_OPTIONS[@]}"
|
||||
echo ""
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Invalid option. Please try again or use 'help' for available commands." >&2
|
||||
echo ""
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Main menu runner function
|
||||
# Usage: menu_run "Menu Title" callback_function "$@"
|
||||
# The menu items array should be defined globally before calling this function
|
||||
function menu_run() {
|
||||
local title="$1"
|
||||
local callback="$2"
|
||||
shift 2
|
||||
|
||||
# Define menu from globally available menu items array
|
||||
# This expects the calling script to have set up the menu items
|
||||
|
||||
# Handle direct execution if arguments provided
|
||||
if [[ $# -gt 0 ]]; then
|
||||
menu_direct_execute "$callback" "$@"
|
||||
return $?
|
||||
fi
|
||||
|
||||
# Run interactive menu
|
||||
menu_interactive "$callback" "$title"
|
||||
}
|
||||
|
||||
# Alternative menu runner that accepts menu items directly
|
||||
# Usage: menu_run_with_items "Menu Title" callback_function -- "${menu_items_array[@]}" -- "$@"
|
||||
function menu_run_with_items() {
|
||||
local title="$1"
|
||||
local callback="$2"
|
||||
shift 2
|
||||
|
||||
# Parse parameters: menu items are between first and second "--"
|
||||
local menu_items=()
|
||||
local script_args=()
|
||||
|
||||
# Skip first "--"
|
||||
if [[ "$1" == "--" ]]; then
|
||||
shift
|
||||
else
|
||||
echo "Error: menu_run_with_items requires -- separator before menu items" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Collect menu items until second "--"
|
||||
while [[ $# -gt 0 && "$1" != "--" ]]; do
|
||||
menu_items+=("$1")
|
||||
shift
|
||||
done
|
||||
|
||||
# Skip second "--" if present
|
||||
if [[ "$1" == "--" ]]; then
|
||||
shift
|
||||
fi
|
||||
|
||||
# Remaining args are script arguments
|
||||
script_args=("$@")
|
||||
|
||||
# Define menu from provided array
|
||||
menu_define "${menu_items[@]}"
|
||||
|
||||
# Handle direct execution if arguments provided
|
||||
if [[ ${#script_args[@]} -gt 0 ]]; then
|
||||
menu_direct_execute "$callback" "${script_args[@]}"
|
||||
return $?
|
||||
fi
|
||||
|
||||
# Run interactive menu
|
||||
menu_interactive "$callback" "$title"
|
||||
}
|
||||
|
||||
# Utility function to show available commands (for --help)
|
||||
# Usage: menu_show_help
|
||||
function menu_show_help() {
|
||||
echo "Available commands:"
|
||||
printf '%s\n' "${_MENU_OPTIONS[@]}"
|
||||
}
|
||||
|
||||
# Utility function to get command key by index
|
||||
# Usage: key=$(menu_get_key index)
|
||||
function menu_get_key() {
|
||||
local idx="$1"
|
||||
if [[ $idx -ge 0 && $idx -lt ${#_MENU_KEYS[@]} ]]; then
|
||||
echo "${_MENU_KEYS[$idx]}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Utility function to get all command keys
|
||||
# Usage: keys=($(menu_get_all_keys))
|
||||
function menu_get_all_keys() {
|
||||
printf '%s\n' "${_MENU_KEYS[@]}"
|
||||
}
|
||||
@@ -5,72 +5,61 @@ set -e
|
||||
CURRENT_PATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
source "$CURRENT_PATH/includes/includes.sh"
|
||||
source "$AC_PATH_APPS/bash_shared/menu_system.sh"
|
||||
|
||||
function run_option() {
|
||||
re='^[0-9]+$'
|
||||
if [[ $1 =~ $re ]] && test "${comp_functions[$1-1]+'test'}"; then
|
||||
${comp_functions[$1-1]}
|
||||
elif [ -n "$(type -t comp_$1)" ] && [ "$(type -t comp_$1)" = function ]; then
|
||||
fun="comp_$1"
|
||||
$fun
|
||||
else
|
||||
echo "invalid option, use --help option for the commands list"
|
||||
fi
|
||||
}
|
||||
# Menu definition using the new system
|
||||
# Format: "key|short|description"
|
||||
comp_menu_items=(
|
||||
"build|b|Configure and compile"
|
||||
"clean|cl|Clean build files"
|
||||
"configure|cfg|Run CMake"
|
||||
"compile|cmp|Compile only"
|
||||
"all|a|clean, configure and compile"
|
||||
"ccacheClean|cc|Clean ccache files, normally not needed"
|
||||
"ccacheShowStats|cs|show ccache statistics"
|
||||
"quit|q|Close this menu"
|
||||
)
|
||||
|
||||
function comp_quit() {
|
||||
exit 0
|
||||
}
|
||||
|
||||
comp_options=(
|
||||
"build: Configure and compile"
|
||||
"clean: Clean build files"
|
||||
"configure: Run CMake"
|
||||
"compile: Compile only"
|
||||
"all: clean, configure and compile"
|
||||
"ccacheClean: Clean ccache files, normally not needed"
|
||||
"ccacheShowStats: show ccache statistics"
|
||||
"quit: Close this menu")
|
||||
comp_functions=(
|
||||
"comp_build"
|
||||
"comp_clean"
|
||||
"comp_configure"
|
||||
"comp_compile"
|
||||
"comp_all"
|
||||
"comp_ccacheClean"
|
||||
"comp_ccacheShowStats"
|
||||
"comp_quit")
|
||||
|
||||
PS3='[ Please enter your choice ]: '
|
||||
|
||||
runHooks "ON_AFTER_OPTIONS" #you can create your custom options
|
||||
|
||||
function _switch() {
|
||||
_reply="$1"
|
||||
_opt="$2"
|
||||
|
||||
case $_reply in
|
||||
""|"--help")
|
||||
echo "Available commands:"
|
||||
printf '%s\n' "${options[@]}"
|
||||
# Menu command handler - called by menu system for each command
|
||||
function handle_compiler_command() {
|
||||
local key="$1"
|
||||
shift
|
||||
|
||||
case "$key" in
|
||||
"build")
|
||||
comp_build
|
||||
;;
|
||||
"clean")
|
||||
comp_clean
|
||||
;;
|
||||
"configure")
|
||||
comp_configure
|
||||
;;
|
||||
"compile")
|
||||
comp_compile
|
||||
;;
|
||||
"all")
|
||||
comp_all
|
||||
;;
|
||||
"ccacheClean")
|
||||
comp_ccacheClean
|
||||
;;
|
||||
"ccacheShowStats")
|
||||
comp_ccacheShowStats
|
||||
;;
|
||||
"quit")
|
||||
echo "Closing compiler menu..."
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
run_option $_reply $_opt
|
||||
;;
|
||||
echo "Invalid option. Use --help to see available commands."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Hook support (preserved from original)
|
||||
runHooks "ON_AFTER_OPTIONS" # you can create your custom options
|
||||
|
||||
while true
|
||||
do
|
||||
# run option directly if specified in argument
|
||||
[ ! -z $1 ] && _switch $@
|
||||
[ ! -z $1 ] && exit 0
|
||||
|
||||
select opt in "${comp_options[@]}"
|
||||
do
|
||||
echo "==== ACORE COMPILER ===="
|
||||
_switch $REPLY
|
||||
break;
|
||||
done
|
||||
done
|
||||
# Run the menu system
|
||||
menu_run_with_items "ACORE COMPILER" handle_compiler_command -- "${comp_menu_items[@]}" -- "$@"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
# Require minimum BATS version to avoid warnings
|
||||
bats_require_minimum_version 1.5.0
|
||||
# Require minimum BATS version when supported (older distro packages lack this)
|
||||
if type -t bats_require_minimum_version >/dev/null 2>&1; then
|
||||
bats_require_minimum_version 1.5.0
|
||||
fi
|
||||
|
||||
# AzerothCore Compiler Scripts Test Suite
|
||||
# Tests the functionality of the compiler scripts using the unified test framework
|
||||
@@ -34,8 +36,8 @@ teardown() {
|
||||
run bash -c "echo '' | timeout 5s $COMPILER_SCRIPT 2>&1 || true"
|
||||
# The script might exit with timeout (124) or success (0), both are acceptable for this test
|
||||
[[ "$status" -eq 0 ]] || [[ "$status" -eq 124 ]]
|
||||
# Check if output contains expected content - looking for menu options
|
||||
[[ "$output" =~ "build:" ]] || [[ "$output" =~ "clean:" ]] || [[ "$output" =~ "Please enter your choice" ]] || [[ -z "$output" ]]
|
||||
# Check if output contains expected content - looking for menu options (old or new format)
|
||||
[[ "$output" =~ "build:" ]] || [[ "$output" =~ "clean:" ]] || [[ "$output" =~ "Please enter your choice" ]] || [[ "$output" =~ "build (b):" ]] || [[ "$output" =~ "ACORE COMPILER" ]] || [[ -z "$output" ]]
|
||||
}
|
||||
|
||||
@test "compiler: should accept option numbers" {
|
||||
@@ -52,16 +54,16 @@ teardown() {
|
||||
|
||||
@test "compiler: should handle invalid option gracefully" {
|
||||
run timeout 5s "$COMPILER_SCRIPT" invalidOption
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" =~ "invalid option" ]]
|
||||
# Should exit with error code for invalid option
|
||||
[ "$status" -eq 1 ]
|
||||
# Output check is optional as error message might be buffered
|
||||
}
|
||||
|
||||
@test "compiler: should handle invalid number gracefully" {
|
||||
run bash -c "echo '999' | timeout 5s $COMPILER_SCRIPT 2>/dev/null || true"
|
||||
# The script might exit with timeout (124) or success (0), both are acceptable
|
||||
run bash -c "echo '999' | timeout 5s $COMPILER_SCRIPT 2>&1 || true"
|
||||
# The script might exit with timeout (124) or success (0) for interactive mode
|
||||
[[ "$status" -eq 0 ]] || [[ "$status" -eq 124 ]]
|
||||
# Check if output contains expected content, or if there's no output due to timeout, that's also acceptable
|
||||
[[ "$output" =~ "invalid option" ]] || [[ "$output" =~ "Please enter your choice" ]] || [[ -z "$output" ]]
|
||||
# In interactive mode, the script should continue asking for input or timeout
|
||||
}
|
||||
|
||||
@test "compiler: should quit with quit option" {
|
||||
|
||||
@@ -118,142 +118,28 @@ function inst_allInOne() {
|
||||
inst_download_client_data
|
||||
}
|
||||
|
||||
function inst_getVersionBranch() {
|
||||
local res="master"
|
||||
local v="not-defined"
|
||||
local MODULE_MAJOR=0
|
||||
local MODULE_MINOR=0
|
||||
local MODULE_PATCH=0
|
||||
local MODULE_SPECIAL=0;
|
||||
local ACV_MAJOR=0
|
||||
local ACV_MINOR=0
|
||||
local ACV_PATCH=0
|
||||
local ACV_SPECIAL=0;
|
||||
local curldata=$(curl -f --silent -H 'Cache-Control: no-cache' "$1" || echo "{}")
|
||||
local parsed=$(echo "$curldata" | "$AC_PATH_DEPS/jsonpath/JSONPath.sh" -b '$.compatibility.*.[version,branch]')
|
||||
############################################################
|
||||
# Module helpers and dispatcher #
|
||||
############################################################
|
||||
|
||||
semverParseInto "$ACORE_VERSION" ACV_MAJOR ACV_MINOR ACV_PATCH ACV_SPECIAL
|
||||
|
||||
if [[ ! -z "$parsed" ]]; then
|
||||
readarray -t vers < <(echo "$parsed")
|
||||
local idx
|
||||
res="none"
|
||||
# since we've the pair version,branch alternated in not associative and one-dimensional
|
||||
# array, we've to simulate the association with length/2 trick
|
||||
for idx in `seq 0 $((${#vers[*]}/2-1))`; do
|
||||
semverParseInto "${vers[idx*2]}" MODULE_MAJOR MODULE_MINOR MODULE_PATCH MODULE_SPECIAL
|
||||
if [[ $MODULE_MAJOR -eq $ACV_MAJOR && $MODULE_MINOR -le $ACV_MINOR ]]; then
|
||||
res="${vers[idx*2+1]}"
|
||||
v="${vers[idx*2]}"
|
||||
fi
|
||||
done
|
||||
# Returns the default branch name of a GitHub repo in the azerothcore org.
|
||||
# If the API call fails, defaults to "master".
|
||||
function inst_get_default_branch() {
|
||||
local repo="$1"
|
||||
local def
|
||||
def=$(curl --silent "https://api.github.com/repos/azerothcore/${repo}" \
|
||||
| "$AC_PATH_DEPS/jsonpath/JSONPath.sh" -b '$.default_branch')
|
||||
if [ -z "$def" ]; then
|
||||
def="master"
|
||||
fi
|
||||
|
||||
echo "$v" "$res"
|
||||
}
|
||||
|
||||
function inst_module_search {
|
||||
|
||||
local res="$1"
|
||||
local idx=0;
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Type what to search or leave blank for full list"
|
||||
read -p "Insert name: " res
|
||||
fi
|
||||
|
||||
local search="+$res"
|
||||
|
||||
echo "Searching $res..."
|
||||
echo "";
|
||||
|
||||
readarray -t MODS < <(curl --silent "https://api.github.com/search/repositories?q=org%3Aazerothcore${search}+fork%3Atrue+topic%3Acore-module+sort%3Astars&type=" \
|
||||
| "$AC_PATH_DEPS/jsonpath/JSONPath.sh" -b '$.items.*.name')
|
||||
while (( ${#MODS[@]} > idx )); do
|
||||
mod="${MODS[idx++]}"
|
||||
read v b < <(inst_getVersionBranch "https://raw.githubusercontent.com/azerothcore/$mod/master/acore-module.json")
|
||||
|
||||
if [[ "$b" != "none" ]]; then
|
||||
echo "-> $mod (tested with AC version: $v)"
|
||||
else
|
||||
echo "-> $mod (no revision available for AC v$AC_VERSION, it could not work!)"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "";
|
||||
echo "";
|
||||
}
|
||||
|
||||
function inst_module_install {
|
||||
local res
|
||||
if [ -z "$1" ]; then
|
||||
echo "Type the name of the module to install"
|
||||
read -p "Insert name: " res
|
||||
else
|
||||
res="$1"
|
||||
fi
|
||||
|
||||
read v b < <(inst_getVersionBranch "https://raw.githubusercontent.com/azerothcore/$res/master/acore-module.json")
|
||||
|
||||
if [[ "$b" != "none" ]]; then
|
||||
Joiner:add_repo "https://github.com/azerothcore/$res" "$res" "$b" && echo "Done, please re-run compiling and db assembly. Read instruction on module repository for more information"
|
||||
else
|
||||
echo "Cannot install $res module: it doesn't exists or no version compatible with AC v$ACORE_VERSION are available"
|
||||
fi
|
||||
|
||||
echo "";
|
||||
echo "";
|
||||
}
|
||||
|
||||
function inst_module_update {
|
||||
local res;
|
||||
local _tmp;
|
||||
local branch;
|
||||
local p;
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Type the name of the module to update"
|
||||
read -p "Insert name: " res
|
||||
else
|
||||
res="$1"
|
||||
fi
|
||||
|
||||
_tmp=$PWD
|
||||
|
||||
if [ -d "$J_PATH_MODULES/$res/" ]; then
|
||||
read v b < <(inst_getVersionBranch "https://raw.githubusercontent.com/azerothcore/$res/master/acore-module.json")
|
||||
|
||||
cd "$J_PATH_MODULES/$res/"
|
||||
|
||||
# use current branch if something wrong with json
|
||||
if [[ "$v" == "none" || "$v" == "not-defined" ]]; then
|
||||
b=`git rev-parse --abbrev-ref HEAD`
|
||||
fi
|
||||
|
||||
Joiner:upd_repo "https://github.com/azerothcore/$res" "$res" "$b" && echo "Done, please re-run compiling and db assembly" || echo "Cannot update"
|
||||
cd $_tmp
|
||||
else
|
||||
echo "Cannot update! Path doesn't exist"
|
||||
fi;
|
||||
|
||||
echo "";
|
||||
echo "";
|
||||
}
|
||||
|
||||
function inst_module_remove {
|
||||
if [ -z "$1" ]; then
|
||||
echo "Type the name of the module to remove"
|
||||
read -p "Insert name: " res
|
||||
else
|
||||
res="$1"
|
||||
fi
|
||||
|
||||
Joiner:remove "$res" && echo "Done, please re-run compiling" || echo "Cannot remove"
|
||||
|
||||
echo "";
|
||||
echo "";
|
||||
echo "$def"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Module Management System
|
||||
# =============================================================================
|
||||
# Load the module manager functions from the dedicated modules-manager directory
|
||||
source "$AC_PATH_INSTALLER/includes/modules-manager/modules.sh"
|
||||
|
||||
function inst_simple_restarter {
|
||||
echo "Running $1 ..."
|
||||
@@ -292,4 +178,4 @@ function inst_download_client_data {
|
||||
&& echo "unzip downloaded file in $path..." && unzip -q -o "$zipPath" -d "$path/" \
|
||||
&& echo "Remove downloaded file" && rm "$zipPath" \
|
||||
&& echo "INSTALLED_VERSION=$VERSION" > "$dataVersionFile"
|
||||
}
|
||||
}
|
||||
311
apps/installer/includes/modules-manager/README.md
Normal file
311
apps/installer/includes/modules-manager/README.md
Normal file
@@ -0,0 +1,311 @@
|
||||
# AzerothCore Module Manager
|
||||
|
||||
This directory contains the module management system for AzerothCore, providing advanced functionality for installing, updating, and managing server modules.
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
- **Advanced Syntax**: Support for `repo[:dirname][@branch[:commit]]` format
|
||||
- **Cross-Format Recognition**: Intelligent matching across URLs, SSH, and simple names
|
||||
- **Custom Directory Naming**: Prevent conflicts with custom directory names
|
||||
- **Duplicate Prevention**: Smart detection and prevention of duplicate installations
|
||||
- **Multi-Host Support**: GitHub, GitLab, and other Git hosts
|
||||
- **Module Exclusion**: Support for excluding modules via environment variable
|
||||
- **Interactive Menu System**: Easy-to-use menu interface for module management
|
||||
- **Colored Output**: Enhanced terminal output with color support (respects NO_COLOR)
|
||||
- **Flat Directory Structure**: Uses flat module installation (no owner subfolders)
|
||||
|
||||
## 📁 File Structure
|
||||
|
||||
```
|
||||
modules-manager/
|
||||
├── modules.sh # Core module management functions
|
||||
└── README.md # This documentation file
|
||||
```
|
||||
|
||||
## 🔧 Module Specification Syntax
|
||||
|
||||
The module manager supports flexible syntax for specifying modules:
|
||||
|
||||
### New Syntax Format
|
||||
```bash
|
||||
repo[:dirname][@branch[:commit]]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
| Specification | Description |
|
||||
|---------------|-------------|
|
||||
| `mod-transmog` | Simple module name, uses default branch and directory |
|
||||
| `mod-transmog:my-custom-dir` | Custom directory name |
|
||||
| `mod-transmog@develop` | Specific branch |
|
||||
| `mod-transmog:custom@develop:abc123` | Custom directory, branch, and commit |
|
||||
| `https://github.com/owner/repo.git@main` | Full URL with branch |
|
||||
| `git@github.com:owner/repo.git:custom-dir` | SSH URL with custom directory |
|
||||
|
||||
## 🎯 Usage Examples
|
||||
|
||||
### Installing Modules
|
||||
|
||||
```bash
|
||||
# Simple module installation
|
||||
./acore.sh module install mod-transmog
|
||||
|
||||
# Install with custom directory name
|
||||
./acore.sh module install mod-transmog:my-transmog-dir
|
||||
|
||||
# Install specific branch
|
||||
./acore.sh module install mod-transmog@develop
|
||||
|
||||
# Install with full specification
|
||||
./acore.sh module install mod-transmog:custom-dir@develop:abc123
|
||||
|
||||
# Install from URL
|
||||
./acore.sh module install https://github.com/azerothcore/mod-transmog.git@main
|
||||
|
||||
# Install multiple modules
|
||||
./acore.sh module install mod-transmog mod-eluna:custom-eluna
|
||||
|
||||
# Install all modules from list
|
||||
./acore.sh module install --all
|
||||
```
|
||||
|
||||
### Updating Modules
|
||||
|
||||
```bash
|
||||
# Update specific module
|
||||
./acore.sh module update mod-transmog
|
||||
|
||||
# Update all modules
|
||||
./acore.sh module update --all
|
||||
|
||||
# Update with branch specification
|
||||
./acore.sh module update mod-transmog@develop
|
||||
```
|
||||
|
||||
### Removing Modules
|
||||
|
||||
```bash
|
||||
# Remove by simple name (cross-format recognition)
|
||||
./acore.sh module remove mod-transmog
|
||||
|
||||
# Remove by URL (recognizes same module)
|
||||
./acore.sh module remove https://github.com/azerothcore/mod-transmog.git
|
||||
|
||||
# Remove multiple modules
|
||||
./acore.sh module remove mod-transmog mod-eluna
|
||||
```
|
||||
|
||||
### Searching Modules
|
||||
|
||||
```bash
|
||||
# Search for modules
|
||||
./acore.sh module search transmog
|
||||
|
||||
# Search with multiple terms
|
||||
./acore.sh module search auction house
|
||||
|
||||
# Search with input prompt
|
||||
./acore.sh module search
|
||||
```
|
||||
|
||||
### Listing Installed Modules
|
||||
|
||||
```bash
|
||||
# List all installed modules
|
||||
./acore.sh module list
|
||||
```
|
||||
|
||||
### Interactive Menu
|
||||
|
||||
```bash
|
||||
# Start interactive menu system
|
||||
./acore.sh module
|
||||
|
||||
# Menu options:
|
||||
# s - Search for available modules
|
||||
# i - Install one or more modules
|
||||
# u - Update installed modules
|
||||
# r - Remove installed modules
|
||||
# l - List installed modules
|
||||
# h - Show detailed help
|
||||
# q - Close this menu
|
||||
```
|
||||
|
||||
## 🔍 Cross-Format Recognition
|
||||
|
||||
The system intelligently recognizes the same module across different specification formats:
|
||||
|
||||
```bash
|
||||
# These all refer to the same module:
|
||||
mod-transmog
|
||||
azerothcore/mod-transmog
|
||||
https://github.com/azerothcore/mod-transmog.git
|
||||
git@github.com:azerothcore/mod-transmog.git
|
||||
```
|
||||
|
||||
This allows:
|
||||
- Installing with one format and removing with another
|
||||
- Preventing duplicates regardless of specification format
|
||||
- Consistent module tracking across different input methods
|
||||
|
||||
## 🛡️ Conflict Prevention
|
||||
|
||||
The system prevents common conflicts:
|
||||
|
||||
### Directory Conflicts
|
||||
```bash
|
||||
# If 'mod-transmog' directory already exists:
|
||||
$ ./acore.sh module install mod-transmog:mod-transmog
|
||||
Possible solutions:
|
||||
1. Use a different directory name: mod-transmog:my-custom-name
|
||||
2. Remove the existing directory first
|
||||
3. Use the update command if this is the same module
|
||||
```
|
||||
|
||||
### Duplicate Module Prevention
|
||||
The system uses intelligent owner/name matching to prevent installing the same module multiple times, even when specified in different formats.
|
||||
|
||||
## 🚫 Module Exclusion
|
||||
|
||||
You can exclude modules from installation using the `MODULES_EXCLUDE_LIST` environment variable:
|
||||
|
||||
```bash
|
||||
# Exclude specific modules (space-separated)
|
||||
export MODULES_EXCLUDE_LIST="mod-test-module azerothcore/mod-dev-only"
|
||||
./acore.sh module install --all # Will skip excluded modules
|
||||
|
||||
# Supports cross-format matching
|
||||
export MODULES_EXCLUDE_LIST="https://github.com/azerothcore/mod-transmog.git"
|
||||
./acore.sh module install mod-transmog # Will be skipped as excluded
|
||||
```
|
||||
|
||||
The exclusion system:
|
||||
- Uses the same cross-format recognition as other module operations
|
||||
- Works with all installation methods (`install`, `install --all`)
|
||||
- Provides clear feedback when modules are skipped
|
||||
- Supports URLs, owner/name format, and simple names
|
||||
|
||||
## 🎨 Color Support
|
||||
|
||||
The module manager provides enhanced terminal output with colors:
|
||||
|
||||
- **Info**: Cyan text for informational messages
|
||||
- **Success**: Green text for successful operations
|
||||
- **Warning**: Yellow text for warnings
|
||||
- **Error**: Red text for errors
|
||||
- **Headers**: Bold cyan text for section headers
|
||||
|
||||
Color support is automatically disabled when:
|
||||
- Output is not to a terminal (piped/redirected)
|
||||
- `NO_COLOR` environment variable is set
|
||||
- Terminal doesn't support colors
|
||||
|
||||
You can force color output with:
|
||||
```bash
|
||||
export FORCE_COLOR=1
|
||||
```
|
||||
|
||||
## 🔄 Integration
|
||||
|
||||
### Including in Scripts
|
||||
```bash
|
||||
# Source the module functions
|
||||
source "$AC_PATH_INSTALLER/includes/modules-manager/modules.sh"
|
||||
|
||||
# Use module functions
|
||||
inst_module_install "mod-transmog:custom-dir@develop"
|
||||
```
|
||||
|
||||
### Testing
|
||||
The module system is tested through the main installer test suite:
|
||||
```bash
|
||||
./apps/installer/test/test_module_commands.bats
|
||||
```
|
||||
|
||||
## 📋 Module List Format
|
||||
|
||||
Modules are tracked in `conf/modules.list` with the format:
|
||||
```
|
||||
# Comments start with #
|
||||
repo_reference branch commit
|
||||
|
||||
# Examples:
|
||||
azerothcore/mod-transmog master abc123def456
|
||||
https://github.com/custom/mod-custom.git develop def456abc789
|
||||
mod-eluna:custom-eluna-dir main 789abc123def
|
||||
```
|
||||
|
||||
The list maintains:
|
||||
- **Alphabetical ordering** by normalized owner/name for consistency
|
||||
- **Original format preservation** of how modules were specified
|
||||
- **Automatic deduplication** across different specification formats
|
||||
- **Custom directory tracking** when specified
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `MODULES_LIST_FILE` | Override default modules list path | `$AC_PATH_ROOT/conf/modules.list` |
|
||||
| `MODULES_EXCLUDE_LIST` | Space-separated list of modules to exclude | - |
|
||||
| `J_PATH_MODULES` | Modules installation directory | `$AC_PATH_ROOT/modules` |
|
||||
| `AC_PATH_ROOT` | AzerothCore root path | - |
|
||||
| `NO_COLOR` | Disable colored output | - |
|
||||
| `FORCE_COLOR` | Force colored output even when not TTY | - |
|
||||
|
||||
### Default Paths
|
||||
- **Modules list**: `$AC_PATH_ROOT/conf/modules.list`
|
||||
- **Installation directory**: `$J_PATH_MODULES` (flat structure, no owner subfolders)
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Core Functions
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `inst_module()` | Main dispatcher and interactive menu |
|
||||
| `inst_parse_module_spec()` | Parse advanced module syntax |
|
||||
| `inst_extract_owner_name()` | Normalize modules for cross-format recognition |
|
||||
| `inst_mod_list_*()` | Module list management (read/write/update) |
|
||||
| `inst_module_*()` | Module operations (install/update/remove/search) |
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Flat Directory Structure**: All modules install directly under `modules/` without owner subdirectories
|
||||
- **Smart Conflict Detection**: Prevents directory name conflicts with helpful suggestions
|
||||
- **Cross-Platform Compatibility**: Works on Linux, macOS, and Windows (Git Bash)
|
||||
- **Version Compatibility**: Checks `acore-module.json` for AzerothCore version compatibility
|
||||
- **Git Integration**: Uses Joiner system for Git repository management
|
||||
|
||||
### Debug Mode
|
||||
|
||||
For debugging module operations, you can examine the generated commands:
|
||||
```bash
|
||||
# Check what Joiner commands would be executed
|
||||
tail -f /tmp/joiner_called.txt # In test environments
|
||||
```
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
When modifying the module manager:
|
||||
|
||||
1. **Maintain backwards compatibility** with existing module list format
|
||||
2. **Update tests** in `test_module_commands.bats` for new functionality
|
||||
3. **Update this documentation** for any new features or changes
|
||||
4. **Test cross-format recognition** thoroughly across all supported formats
|
||||
5. **Ensure helpful error messages** for common user mistakes
|
||||
6. **Test exclusion functionality** with various module specification formats
|
||||
7. **Verify color output** works correctly in different terminal environments
|
||||
|
||||
### Testing Guidelines
|
||||
|
||||
```bash
|
||||
# Run all module-related tests
|
||||
cd apps/installer
|
||||
bats test/test_module_commands.bats
|
||||
|
||||
# Test with different environments
|
||||
NO_COLOR=1 ./acore.sh module list
|
||||
FORCE_COLOR=1 ./acore.sh module help
|
||||
```
|
||||
7
apps/installer/includes/modules-manager/module-main.sh
Normal file
7
apps/installer/includes/modules-manager/module-main.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
CURRENT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" || exit ; pwd )
|
||||
|
||||
source "$CURRENT_PATH/modules.sh"
|
||||
|
||||
inst_module "$@"
|
||||
1029
apps/installer/includes/modules-manager/modules.sh
Normal file
1029
apps/installer/includes/modules-manager/modules.sh
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,105 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# AzerothCore Dashboard Script
|
||||
#
|
||||
# This script provides an interactive menu system for AzerothCore management
|
||||
# using the unified menu system library.
|
||||
#
|
||||
# Usage:
|
||||
# ./acore.sh - Interactive mode with numeric and text selection
|
||||
# ./acore.sh <command> [args] - Direct command execution (only text commands, no numbers)
|
||||
#
|
||||
# Interactive Mode:
|
||||
# - Select options by number (1, 2, 3...), command name (init, compiler, etc.),
|
||||
# or short alias (i, c, etc.)
|
||||
# - All selection methods work in interactive mode
|
||||
#
|
||||
# Direct Command Mode:
|
||||
# - Only command names and short aliases are accepted (e.g., './acore.sh compiler build', './acore.sh c build')
|
||||
# - Numeric selection is disabled to prevent confusion with command arguments
|
||||
# - Examples: './acore.sh init', './acore.sh compiler clean', './acore.sh module install mod-name'
|
||||
#
|
||||
# Menu System:
|
||||
# - Uses unified menu system from bash_shared/menu_system.sh
|
||||
# - Single source of truth for menu definitions
|
||||
# - Consistent behavior across all AzerothCore tools
|
||||
|
||||
CURRENT_PATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
source "$CURRENT_PATH/includes/includes.sh"
|
||||
source "$AC_PATH_APPS/bash_shared/menu_system.sh"
|
||||
|
||||
PS3='[Please enter your choice]: '
|
||||
options=(
|
||||
"init (i): First Installation" # 1
|
||||
"install-deps (d): Configure OS dep" # 2
|
||||
"pull (u): Update Repository" # 3
|
||||
"reset (r): Reset & Clean Repository" # 4
|
||||
"compiler (c): Run compiler tool" # 5
|
||||
"module-search (ms): Module Search by keyword" # 6
|
||||
"module-install (mi): Module Install by name" # 7
|
||||
"module-update (mu): Module Update by name" # 8
|
||||
"module-remove: (mr): Module Remove by name" # 9
|
||||
"client-data: (gd): download client data from github repository (beta)" # 10
|
||||
"run-worldserver (rw): execute a simple restarter for worldserver" # 11
|
||||
"run-authserver (ra): execute a simple restarter for authserver" # 12
|
||||
"docker (dr): Run docker tools" # 13
|
||||
"version (v): Show AzerothCore version" # 14
|
||||
"service-manager (sm): Run service manager to run authserver and worldserver in background" # 15
|
||||
"quit: Exit from this menu" # 16
|
||||
)
|
||||
# Menu: single ordered source of truth (no functions in strings)
|
||||
# Format: "key|short|description"
|
||||
menu_items=(
|
||||
"init|i|First Installation"
|
||||
"install-deps|d|Configure OS dep"
|
||||
"pull|u|Update Repository"
|
||||
"reset|r|Reset & Clean Repository"
|
||||
"compiler|c|Run compiler tool"
|
||||
"module|m|Module manager (search/install/update/remove)"
|
||||
"client-data|gd|download client data from github repository (beta)"
|
||||
"run-worldserver|rw|execute a simple restarter for worldserver"
|
||||
"run-authserver|ra|execute a simple restarter for authserver"
|
||||
"docker|dr|Run docker tools"
|
||||
"version|v|Show AzerothCore version"
|
||||
"service-manager|sm|Run service manager to run authserver and worldserver in background"
|
||||
"quit|q|Exit from this menu"
|
||||
)
|
||||
|
||||
function _switch() {
|
||||
_reply="$1"
|
||||
_opt="$2"
|
||||
|
||||
case $_reply in
|
||||
""|"i"|"init"|"1")
|
||||
inst_allInOne
|
||||
# Menu command handler - called by menu system for each command
|
||||
function handle_menu_command() {
|
||||
local key="$1"
|
||||
shift
|
||||
|
||||
case "$key" in
|
||||
"init")
|
||||
inst_allInOne
|
||||
;;
|
||||
""|"d"|"install-deps"|"2")
|
||||
inst_configureOS
|
||||
"install-deps")
|
||||
inst_configureOS
|
||||
;;
|
||||
""|"u"|"pull"|"3")
|
||||
inst_updateRepo
|
||||
"pull")
|
||||
inst_updateRepo
|
||||
;;
|
||||
""|"r"|"reset"|"4")
|
||||
inst_resetRepo
|
||||
"reset")
|
||||
inst_resetRepo
|
||||
;;
|
||||
""|"c"|"compiler"|"5")
|
||||
bash "$AC_PATH_APPS/compiler/compiler.sh" $_opt
|
||||
"compiler")
|
||||
bash "$AC_PATH_APPS/compiler/compiler.sh" "$@"
|
||||
;;
|
||||
""|"ms"|"module-search"|"6")
|
||||
inst_module_search "$_opt"
|
||||
"module")
|
||||
bash "$AC_PATH_APPS/installer/includes/modules-manager/module-main.sh" "$@"
|
||||
;;
|
||||
""|"mi"|"module-install"|"7")
|
||||
inst_module_install "$_opt"
|
||||
"client-data")
|
||||
inst_download_client_data
|
||||
;;
|
||||
""|"mu"|"module-update"|"8")
|
||||
inst_module_update "$_opt"
|
||||
"run-worldserver")
|
||||
inst_simple_restarter worldserver
|
||||
;;
|
||||
""|"mr"|"module-remove"|"9")
|
||||
inst_module_remove "$_opt"
|
||||
"run-authserver")
|
||||
inst_simple_restarter authserver
|
||||
;;
|
||||
""|"gd"|"client-data"|"10")
|
||||
inst_download_client_data
|
||||
"docker")
|
||||
DOCKER=1 bash "$AC_PATH_ROOT/apps/docker/docker-cmd.sh" "$@"
|
||||
exit
|
||||
;;
|
||||
""|"rw"|"run-worldserver"|"11")
|
||||
inst_simple_restarter worldserver
|
||||
;;
|
||||
""|"ra"|"run-authserver"|"12")
|
||||
inst_simple_restarter authserver
|
||||
;;
|
||||
""|"dr"|"docker"|"13")
|
||||
DOCKER=1 bash "$AC_PATH_ROOT/apps/docker/docker-cmd.sh" "${@:2}"
|
||||
exit
|
||||
;;
|
||||
""|"v"|"version"|"14")
|
||||
# denoRunFile "$AC_PATH_APPS/installer/main.ts" "version"
|
||||
"version")
|
||||
printf "AzerothCore Rev. %s\n" "$ACORE_VERSION"
|
||||
exit
|
||||
exit
|
||||
;;
|
||||
""|"sm"|"service-manager"|"15")
|
||||
bash "$AC_PATH_APPS/startup-scripts/src/service-manager.sh" "${@:2}"
|
||||
exit
|
||||
"service-manager")
|
||||
bash "$AC_PATH_APPS/startup-scripts/src/service-manager.sh" "$@"
|
||||
exit
|
||||
;;
|
||||
""|"quit"|"16")
|
||||
"quit")
|
||||
echo "Goodbye!"
|
||||
exit
|
||||
exit
|
||||
;;
|
||||
""|"--help")
|
||||
echo "Available commands:"
|
||||
printf '%s\n' "${options[@]}"
|
||||
*)
|
||||
echo "Invalid option. Use --help to see available commands."
|
||||
return 1
|
||||
;;
|
||||
*) echo "invalid option, use --help option for the commands list";;
|
||||
esac
|
||||
}
|
||||
|
||||
while true
|
||||
do
|
||||
# run option directly if specified in argument
|
||||
[ ! -z $1 ] && _switch $@ # old method: "${options[$cmdopt-1]}"
|
||||
[ ! -z $1 ] && exit 0
|
||||
|
||||
echo "==== ACORE DASHBOARD ===="
|
||||
select opt in "${options[@]}"
|
||||
do
|
||||
_switch $REPLY
|
||||
break
|
||||
done
|
||||
done
|
||||
# Run the menu system
|
||||
menu_run_with_items "ACORE DASHBOARD" handle_menu_command -- "${menu_items[@]}" -- "$@"
|
||||
|
||||
14
apps/installer/test/bats.conf
Normal file
14
apps/installer/test/bats.conf
Normal file
@@ -0,0 +1,14 @@
|
||||
# BATS Test Configuration
|
||||
|
||||
# Set test timeout (in seconds)
|
||||
export BATS_TEST_TIMEOUT=30
|
||||
|
||||
# Enable verbose output for debugging
|
||||
export BATS_VERBOSE_RUN=1
|
||||
|
||||
# Test output format
|
||||
export BATS_FORMATTER=pretty
|
||||
|
||||
# Enable colored output
|
||||
export BATS_NO_PARALLELIZE_ACROSS_FILES=1
|
||||
export BATS_NO_PARALLELIZE_WITHIN_FILE=1
|
||||
755
apps/installer/test/test_module_commands.bats
Executable file
755
apps/installer/test/test_module_commands.bats
Executable file
@@ -0,0 +1,755 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
# Tests for installer module commands (search/install/update/remove)
|
||||
# Focused on installer:module install behavior using a mocked joiner
|
||||
|
||||
load '../../test-framework/bats_libs/acore-support'
|
||||
load '../../test-framework/bats_libs/acore-assert'
|
||||
|
||||
setup() {
|
||||
acore_test_setup
|
||||
# Point to the installer src directory (not needed in this test)
|
||||
|
||||
# Set installer/paths environment for the test
|
||||
export AC_PATH_APPS="$TEST_DIR/apps"
|
||||
export AC_PATH_ROOT="$TEST_DIR"
|
||||
export AC_PATH_DEPS="$TEST_DIR/deps"
|
||||
export AC_PATH_MODULES="$TEST_DIR/modules"
|
||||
export MODULES_LIST_FILE="$TEST_DIR/conf/modules.list"
|
||||
|
||||
# Create stubbed deps: joiner.sh (sourced by includes) and semver
|
||||
mkdir -p "$TEST_DIR/deps/acore/joiner"
|
||||
cat > "$TEST_DIR/deps/acore/joiner/joiner.sh" << 'EOF'
|
||||
#!/usr/bin/env bash
|
||||
# Stub joiner functions used by installer
|
||||
Joiner:add_repo() {
|
||||
# arguments: url name branch basedir
|
||||
echo "ADD $@" > "$TEST_DIR/joiner_called.txt"
|
||||
return 0
|
||||
}
|
||||
Joiner:upd_repo() {
|
||||
echo "UPD $@" > "$TEST_DIR/joiner_called.txt"
|
||||
return 0
|
||||
}
|
||||
Joiner:remove() {
|
||||
echo "REM $@" > "$TEST_DIR/joiner_called.txt"
|
||||
return 0
|
||||
}
|
||||
EOF
|
||||
chmod +x "$TEST_DIR/deps/acore/joiner/joiner.sh"
|
||||
|
||||
mkdir -p "$TEST_DIR/deps/semver_bash"
|
||||
# Minimal semver stub
|
||||
cat > "$TEST_DIR/deps/semver_bash/semver.sh" << 'EOF'
|
||||
#!/usr/bin/env bash
|
||||
# semver stub
|
||||
semver::satisfies() { return 0; }
|
||||
EOF
|
||||
chmod +x "$TEST_DIR/deps/semver_bash/semver.sh"
|
||||
|
||||
# Provide a minimal compiler includes file expected by installer
|
||||
mkdir -p "$TEST_DIR/apps/compiler/includes"
|
||||
touch "$TEST_DIR/apps/compiler/includes/includes.sh"
|
||||
|
||||
# Provide minimal bash_shared includes to satisfy installer include
|
||||
mkdir -p "$TEST_DIR/apps/bash_shared"
|
||||
cat > "$TEST_DIR/apps/bash_shared/includes.sh" << 'EOF'
|
||||
#!/usr/bin/env bash
|
||||
# minimal stub
|
||||
EOF
|
||||
|
||||
# Copy the menu system needed by modules.sh
|
||||
cp "$AC_TEST_ROOT/apps/bash_shared/menu_system.sh" "$TEST_DIR/apps/bash_shared/"
|
||||
|
||||
# Copy the real installer app into the test apps dir
|
||||
mkdir -p "$TEST_DIR/apps"
|
||||
cp -r "$(cd "$AC_TEST_ROOT/apps/installer" && pwd)" "$TEST_DIR/apps/installer"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
acore_test_teardown
|
||||
}
|
||||
|
||||
@test "module install should call joiner and record entry in modules list" {
|
||||
cd "$TEST_DIR"
|
||||
|
||||
# Source installer includes and call the install function directly to avoid menu interaction
|
||||
run bash -c "source '$TEST_DIR/apps/installer/includes/includes.sh' && inst_module_install example-module@main:abcd1234"
|
||||
|
||||
# Check that joiner was called
|
||||
[ -f "$TEST_DIR/joiner_called.txt" ]
|
||||
grep -q "ADD" "$TEST_DIR/joiner_called.txt"
|
||||
|
||||
# Check modules list was created and contains the repo_ref and branch
|
||||
[ -f "$TEST_DIR/conf/modules.list" ]
|
||||
grep -q "azerothcore/example-module main" "$TEST_DIR/conf/modules.list"
|
||||
}
|
||||
|
||||
@test "module install with owner/name format should work" {
|
||||
cd "$TEST_DIR"
|
||||
|
||||
# Test with owner/name format
|
||||
run bash -c "source '$TEST_DIR/apps/installer/includes/includes.sh' && inst_module_install myorg/mymodule"
|
||||
|
||||
# Check that joiner was called with correct URL
|
||||
[ -f "$TEST_DIR/joiner_called.txt" ]
|
||||
grep -q "ADD https://github.com/myorg/mymodule mymodule" "$TEST_DIR/joiner_called.txt"
|
||||
|
||||
# Check modules list contains the entry
|
||||
[ -f "$TEST_DIR/conf/modules.list" ]
|
||||
grep -q "myorg/mymodule" "$TEST_DIR/conf/modules.list"
|
||||
}
|
||||
|
||||
@test "module remove should call joiner remove and update modules list" {
|
||||
cd "$TEST_DIR"
|
||||
|
||||
# First install a module
|
||||
bash -c "source '$TEST_DIR/apps/installer/includes/includes.sh' && inst_module_install test-module"
|
||||
|
||||
# Then remove it
|
||||
run bash -c "source '$TEST_DIR/apps/installer/includes/includes.sh' && inst_module_remove test-module"
|
||||
|
||||
# Check that joiner remove was called
|
||||
[ -f "$TEST_DIR/joiner_called.txt" ]
|
||||
# With flat structure, basedir is empty; ensure name is present
|
||||
grep -q "REM test-module" "$TEST_DIR/joiner_called.txt"
|
||||
|
||||
# Check modules list no longer contains the entry
|
||||
[ -f "$TEST_DIR/conf/modules.list" ]
|
||||
! grep -q "azerothcore/test-module" "$TEST_DIR/conf/modules.list"
|
||||
}
|
||||
|
||||
# Tests for intelligent module management (duplicate prevention and cross-format removal)
|
||||
|
||||
@test "inst_extract_owner_name should extract owner/name from various formats" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Test simple name
|
||||
run inst_extract_owner_name "mod-transmog"
|
||||
[ "$output" = "azerothcore/mod-transmog" ]
|
||||
|
||||
# Test owner/name format
|
||||
run inst_extract_owner_name "azerothcore/mod-transmog"
|
||||
[ "$output" = "azerothcore/mod-transmog" ]
|
||||
|
||||
# Test HTTPS URL
|
||||
run inst_extract_owner_name "https://github.com/azerothcore/mod-transmog.git"
|
||||
[ "$output" = "azerothcore/mod-transmog" ]
|
||||
|
||||
# Test SSH URL
|
||||
run inst_extract_owner_name "git@github.com:azerothcore/mod-transmog.git"
|
||||
[ "$output" = "azerothcore/mod-transmog" ]
|
||||
|
||||
# Test GitLab URL
|
||||
run inst_extract_owner_name "https://gitlab.com/myorg/mymodule.git"
|
||||
[ "$output" = "myorg/mymodule" ]
|
||||
}
|
||||
|
||||
@test "inst_extract_owner_name should handle URLs with ports correctly" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Test HTTPS URL with port
|
||||
run inst_extract_owner_name "https://example.com:8080/user/repo.git"
|
||||
[ "$output" = "user/repo" ]
|
||||
|
||||
# Test SSH URL with port
|
||||
run inst_extract_owner_name "ssh://git@example.com:2222/owner/module"
|
||||
[ "$output" = "owner/module" ]
|
||||
|
||||
# Test URL with port and custom directory (should ignore the directory part)
|
||||
run inst_extract_owner_name "https://gitlab.internal:9443/team/project.git:custom-dir"
|
||||
[ "$output" = "team/project" ]
|
||||
|
||||
# Test complex URL with port (should extract owner/name correctly)
|
||||
run inst_extract_owner_name "https://git.company.com:8443/department/awesome-module.git"
|
||||
[ "$output" = "department/awesome-module" ]
|
||||
}
|
||||
|
||||
@test "duplicate module entries should be prevented across different formats" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Add module via simple name
|
||||
inst_mod_list_upsert "mod-transmog" "master" "abc123"
|
||||
|
||||
# Verify it's in the list
|
||||
grep -q "mod-transmog master abc123" "$TEST_DIR/conf/modules.list"
|
||||
|
||||
# Add same module via owner/name format - should replace, not duplicate
|
||||
inst_mod_list_upsert "azerothcore/mod-transmog" "dev" "def456"
|
||||
|
||||
# Should only have one entry (the new one)
|
||||
[ "$(grep -c "azerothcore/mod-transmog" "$TEST_DIR/conf/modules.list")" -eq 1 ]
|
||||
grep -q "azerothcore/mod-transmog dev def456" "$TEST_DIR/conf/modules.list"
|
||||
! grep -q "mod-transmog master abc123" "$TEST_DIR/conf/modules.list"
|
||||
}
|
||||
|
||||
@test "module installed via URL should be recognized when checking with different formats" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Install via HTTPS URL
|
||||
inst_mod_list_upsert "https://github.com/azerothcore/mod-transmog.git" "master" "abc123"
|
||||
|
||||
# Should be detected as installed using simple name
|
||||
run inst_mod_is_installed "mod-transmog"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
# Should be detected as installed using owner/name
|
||||
run inst_mod_is_installed "azerothcore/mod-transmog"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
# Should be detected as installed using SSH URL
|
||||
run inst_mod_is_installed "git@github.com:azerothcore/mod-transmog.git"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
# Non-existent module should not be detected
|
||||
run inst_mod_is_installed "mod-nonexistent"
|
||||
[ "$status" -ne 0 ]
|
||||
}
|
||||
|
||||
@test "module installed via URL with port should be recognized correctly" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Install via URL with port
|
||||
inst_mod_list_upsert "https://gitlab.internal:9443/myorg/my-module.git" "master" "abc123"
|
||||
|
||||
# Should be detected as installed using normalized owner/name
|
||||
run inst_mod_is_installed "myorg/my-module"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
# Should be detected when checking with different URL format
|
||||
run inst_mod_is_installed "ssh://git@gitlab.internal:9443/myorg/my-module"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
# Should be detected when checking with custom directory syntax
|
||||
run inst_mod_is_installed "myorg/my-module:custom-dir"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
# Different module should not be detected
|
||||
run inst_mod_is_installed "myorg/different-module"
|
||||
[ "$status" -ne 0 ]
|
||||
}
|
||||
|
||||
@test "cross-format module removal should work" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Install via SSH URL
|
||||
inst_mod_list_upsert "git@github.com:azerothcore/mod-transmog.git" "master" "abc123"
|
||||
|
||||
# Verify it's installed
|
||||
grep -q "git@github.com:azerothcore/mod-transmog.git" "$TEST_DIR/conf/modules.list"
|
||||
|
||||
# Remove using simple name
|
||||
inst_mod_list_remove "mod-transmog"
|
||||
|
||||
# Should be completely removed
|
||||
! grep -q "azerothcore/mod-transmog" "$TEST_DIR/conf/modules.list"
|
||||
! grep -q "git@github.com:azerothcore/mod-transmog.git" "$TEST_DIR/conf/modules.list"
|
||||
}
|
||||
|
||||
@test "module installation should prevent duplicates when already installed" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Install via simple name first
|
||||
inst_mod_list_upsert "mod-worldchat" "master" "abc123"
|
||||
|
||||
# Try to install same module via URL - should detect it's already installed
|
||||
run inst_mod_is_installed "https://github.com/azerothcore/mod-worldchat.git"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
# Add via URL should replace the existing entry
|
||||
inst_mod_list_upsert "https://github.com/azerothcore/mod-worldchat.git" "dev" "def456"
|
||||
|
||||
# Should only have one entry
|
||||
[ "$(grep -c "azerothcore/mod-worldchat" "$TEST_DIR/conf/modules.list")" -eq 1 ]
|
||||
grep -q "https://github.com/azerothcore/mod-worldchat.git dev def456" "$TEST_DIR/conf/modules.list"
|
||||
}
|
||||
|
||||
@test "module update --all uses flat structure (no branch subfolders)" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Prepare modules.list with one entry and a matching local directory
|
||||
mkdir -p "$TEST_DIR/conf"
|
||||
echo "azerothcore/mod-transmog master abc123" > "$TEST_DIR/conf/modules.list"
|
||||
mkdir -p "$TEST_DIR/modules/mod-transmog"
|
||||
|
||||
# Run update all
|
||||
run bash -c "source '$TEST_DIR/apps/installer/includes/includes.sh' && inst_module_update --all"
|
||||
|
||||
# Verify Joiner:upd_repo received flat structure args (no basedir)
|
||||
[ -f "$TEST_DIR/joiner_called.txt" ]
|
||||
grep -q "UPD https://github.com/azerothcore/mod-transmog mod-transmog master" "$TEST_DIR/joiner_called.txt"
|
||||
}
|
||||
|
||||
@test "module update specific uses flat structure with override branch" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Create local directory so update proceeds
|
||||
mkdir -p "$TEST_DIR/modules/mymodule"
|
||||
|
||||
# Run update specifying owner/name and branch
|
||||
run bash -c "source '$TEST_DIR/apps/installer/includes/includes.sh' && inst_module_update myorg/mymodule@dev"
|
||||
|
||||
# Should call joiner with name 'mymodule' and branch 'dev' (no basedir)
|
||||
[ -f "$TEST_DIR/joiner_called.txt" ]
|
||||
grep -q "UPD https://github.com/myorg/mymodule mymodule dev" "$TEST_DIR/joiner_called.txt"
|
||||
}
|
||||
|
||||
@test "custom directory names should work with new syntax" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Test parsing with custom directory name
|
||||
run inst_parse_module_spec "mod-transmog:my-custom-dir@develop:abc123"
|
||||
[ "$status" -eq 0 ]
|
||||
# Should output: repo_ref owner name branch commit url dirname
|
||||
IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output"
|
||||
[ "$repo_ref" = "azerothcore/mod-transmog" ]
|
||||
[ "$owner" = "azerothcore" ]
|
||||
[ "$name" = "mod-transmog" ]
|
||||
[ "$branch" = "develop" ]
|
||||
[ "$commit" = "abc123" ]
|
||||
[ "$url" = "https://github.com/azerothcore/mod-transmog" ]
|
||||
[ "$dirname" = "my-custom-dir" ]
|
||||
}
|
||||
|
||||
@test "directory conflict detection should work" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Create a fake existing directory
|
||||
mkdir -p "$TEST_DIR/modules/existing-dir"
|
||||
|
||||
# Should detect conflict
|
||||
run inst_check_module_conflict "existing-dir" "mod-test"
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" =~ "Directory 'existing-dir' already exists" ]]
|
||||
[[ "$output" =~ "Use a different directory name: mod-test:my-custom-name" ]]
|
||||
|
||||
# Should not detect conflict for non-existing directory
|
||||
run inst_check_module_conflict "non-existing-dir" "mod-test"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "module update should work with custom directories" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# First add module with custom directory to list
|
||||
inst_mod_list_upsert "azerothcore/mod-transmog:custom-dir" "master" "abc123"
|
||||
|
||||
# Create fake module directory structure
|
||||
mkdir -p "$TEST_DIR/modules/custom-dir/.git"
|
||||
echo "ref: refs/heads/master" > "$TEST_DIR/modules/custom-dir/.git/HEAD"
|
||||
|
||||
# Mock git commands in the fake module directory
|
||||
cat > "$TEST_DIR/modules/custom-dir/.git/config" << 'EOF'
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = false
|
||||
[remote "origin"]
|
||||
url = https://github.com/azerothcore/mod-transmog
|
||||
fetch = +refs/heads/*:refs/remotes/origin/*
|
||||
[branch "master"]
|
||||
remote = origin
|
||||
merge = refs/heads/master
|
||||
EOF
|
||||
|
||||
# Test update with custom directory should work
|
||||
# Note: This would require more complex mocking for full integration test
|
||||
# For now, just test the parsing recognizes the custom directory
|
||||
run inst_parse_module_spec "azerothcore/mod-transmog:custom-dir"
|
||||
[ "$status" -eq 0 ]
|
||||
IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output"
|
||||
[ "$dirname" = "custom-dir" ]
|
||||
}
|
||||
|
||||
@test "URL formats should be properly normalized" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Test various URL formats produce same owner/name
|
||||
run inst_extract_owner_name "https://github.com/azerothcore/mod-transmog"
|
||||
local url_format="$output"
|
||||
|
||||
run inst_extract_owner_name "https://github.com/azerothcore/mod-transmog.git"
|
||||
local url_git_format="$output"
|
||||
|
||||
run inst_extract_owner_name "git@github.com:azerothcore/mod-transmog.git"
|
||||
local ssh_format="$output"
|
||||
|
||||
run inst_extract_owner_name "azerothcore/mod-transmog"
|
||||
local owner_name_format="$output"
|
||||
|
||||
run inst_extract_owner_name "mod-transmog"
|
||||
local simple_format="$output"
|
||||
|
||||
# All should normalize to the same owner/name
|
||||
[ "$url_format" = "azerothcore/mod-transmog" ]
|
||||
[ "$url_git_format" = "azerothcore/mod-transmog" ]
|
||||
[ "$ssh_format" = "azerothcore/mod-transmog" ]
|
||||
[ "$owner_name_format" = "azerothcore/mod-transmog" ]
|
||||
[ "$simple_format" = "azerothcore/mod-transmog" ]
|
||||
}
|
||||
|
||||
# Tests for module exclusion functionality
|
||||
|
||||
@test "module exclusion should work with MODULES_EXCLUDE_LIST" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Test exclusion with simple name
|
||||
export MODULES_EXCLUDE_LIST="mod-test-module"
|
||||
run inst_mod_is_excluded "mod-test-module"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
# Test exclusion with owner/name format
|
||||
export MODULES_EXCLUDE_LIST="azerothcore/mod-test"
|
||||
run inst_mod_is_excluded "mod-test"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
# Test exclusion with space-separated list
|
||||
export MODULES_EXCLUDE_LIST="mod-one mod-two mod-three"
|
||||
run inst_mod_is_excluded "mod-two"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
# Test exclusion with newline-separated list
|
||||
export MODULES_EXCLUDE_LIST="
|
||||
mod-alpha
|
||||
mod-beta
|
||||
mod-gamma
|
||||
"
|
||||
run inst_mod_is_excluded "mod-beta"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
# Test exclusion with URL format
|
||||
export MODULES_EXCLUDE_LIST="https://github.com/azerothcore/mod-transmog.git"
|
||||
run inst_mod_is_excluded "mod-transmog"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
# Test non-excluded module
|
||||
export MODULES_EXCLUDE_LIST="mod-other"
|
||||
run inst_mod_is_excluded "mod-transmog"
|
||||
[ "$status" -eq 1 ]
|
||||
|
||||
# Test empty exclusion list
|
||||
unset MODULES_EXCLUDE_LIST
|
||||
run inst_mod_is_excluded "mod-transmog"
|
||||
[ "$status" -eq 1 ]
|
||||
}
|
||||
|
||||
@test "install --all should skip excluded modules" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Setup modules list with excluded module
|
||||
mkdir -p "$TEST_DIR/conf"
|
||||
cat > "$TEST_DIR/conf/modules.list" << 'EOF'
|
||||
azerothcore/mod-transmog master abc123
|
||||
azerothcore/mod-excluded master def456
|
||||
EOF
|
||||
|
||||
# Set exclusion list
|
||||
export MODULES_EXCLUDE_LIST="mod-excluded"
|
||||
|
||||
# Mock the install process to capture output
|
||||
run bash -c "source '$TEST_DIR/apps/installer/includes/includes.sh' && inst_module_install --all 2>&1"
|
||||
|
||||
# Should show that excluded module was skipped
|
||||
[[ "$output" == *"azerothcore/mod-excluded"* && "$output" == *"Excluded by MODULES_EXCLUDE_LIST"* && "$output" == *"skipping"* ]]
|
||||
}
|
||||
|
||||
@test "exclusion should work with multiple formats in same list" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Test multiple exclusion formats
|
||||
export MODULES_EXCLUDE_LIST="mod-test https://github.com/azerothcore/mod-transmog.git custom/mod-other"
|
||||
|
||||
run inst_mod_is_excluded "mod-test"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run inst_mod_is_excluded "azerothcore/mod-transmog"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run inst_mod_is_excluded "custom/mod-other"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run inst_mod_is_excluded "mod-allowed"
|
||||
[ "$status" -eq 1 ]
|
||||
}
|
||||
|
||||
# Tests for color support functionality
|
||||
|
||||
@test "color functions should work correctly" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Test that print functions exist and work
|
||||
run print_info "test message"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run print_warn "test warning"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run print_error "test error"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run print_success "test success"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run print_skip "test skip"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run print_header "test header"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "color support should respect NO_COLOR environment variable" {
|
||||
cd "$TEST_DIR"
|
||||
|
||||
# Test with NO_COLOR set
|
||||
export NO_COLOR=1
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Colors should be empty when NO_COLOR is set
|
||||
[ -z "$C_RED" ]
|
||||
[ -z "$C_GREEN" ]
|
||||
[ -z "$C_RESET" ]
|
||||
}
|
||||
|
||||
# Tests for interactive menu system
|
||||
|
||||
@test "module help should display comprehensive help" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
run inst_module_help
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
# Should contain key sections
|
||||
[[ "$output" =~ "Module Manager Help" ]]
|
||||
[[ "$output" =~ "Usage:" ]]
|
||||
[[ "$output" =~ "Module Specification Syntax:" ]]
|
||||
[[ "$output" =~ "Examples:" ]]
|
||||
}
|
||||
|
||||
@test "module list should show installed modules correctly" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Setup modules list
|
||||
mkdir -p "$TEST_DIR/conf"
|
||||
cat > "$TEST_DIR/conf/modules.list" << 'EOF'
|
||||
azerothcore/mod-transmog master abc123
|
||||
custom/mod-test develop def456
|
||||
EOF
|
||||
|
||||
run inst_module_list
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
# Should show both modules
|
||||
[[ "$output" =~ "mod-transmog" ]]
|
||||
[[ "$output" =~ "custom/mod-test" ]]
|
||||
[[ "$output" =~ "master" ]]
|
||||
[[ "$output" =~ "develop" ]]
|
||||
}
|
||||
|
||||
@test "module list should handle empty list gracefully" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Ensure empty modules list
|
||||
mkdir -p "$TEST_DIR/conf"
|
||||
touch "$TEST_DIR/conf/modules.list"
|
||||
|
||||
run inst_module_list
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" =~ "No modules installed" ]]
|
||||
}
|
||||
|
||||
# Tests for advanced parsing edge cases
|
||||
|
||||
@test "parsing should handle complex URL formats" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Test GitLab URL with custom directory and branch
|
||||
run inst_parse_module_spec "https://gitlab.com/myorg/mymodule.git:custom-dir@develop:abc123"
|
||||
[ "$status" -eq 0 ]
|
||||
IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output"
|
||||
[ "$repo_ref" = "https://gitlab.com/myorg/mymodule.git" ]
|
||||
[ "$owner" = "myorg" ]
|
||||
[ "$name" = "mymodule" ]
|
||||
[ "$branch" = "develop" ]
|
||||
[ "$commit" = "abc123" ]
|
||||
[ "$dirname" = "custom-dir" ]
|
||||
}
|
||||
|
||||
@test "parsing should handle URLs with ports correctly (fix for port/dirname confusion)" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Test HTTPS URL with port - should NOT treat port as dirname
|
||||
run inst_parse_module_spec "https://example.com:8080/user/repo.git"
|
||||
[ "$status" -eq 0 ]
|
||||
IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output"
|
||||
[ "$repo_ref" = "https://example.com:8080/user/repo.git" ]
|
||||
[ "$owner" = "user" ]
|
||||
[ "$name" = "repo" ]
|
||||
[ "$branch" = "-" ]
|
||||
[ "$commit" = "-" ]
|
||||
[ "$url" = "https://example.com:8080/user/repo.git" ]
|
||||
[ "$dirname" = "repo" ] # Should default to repo name, NOT port number
|
||||
}
|
||||
|
||||
@test "parsing should handle URLs with ports and custom directory correctly" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Test URL with port AND custom directory - should parse custom directory correctly
|
||||
run inst_parse_module_spec "https://example.com:8080/user/repo.git:custom-dir"
|
||||
[ "$status" -eq 0 ]
|
||||
IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output"
|
||||
[ "$repo_ref" = "https://example.com:8080/user/repo.git" ]
|
||||
[ "$owner" = "user" ]
|
||||
[ "$name" = "repo" ]
|
||||
[ "$branch" = "-" ]
|
||||
[ "$commit" = "-" ]
|
||||
[ "$url" = "https://example.com:8080/user/repo.git" ]
|
||||
[ "$dirname" = "custom-dir" ] # Should be custom-dir, not port number
|
||||
}
|
||||
|
||||
@test "parsing should handle SSH URLs with ports correctly" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Test SSH URL with port
|
||||
run inst_parse_module_spec "ssh://git@example.com:2222/user/repo"
|
||||
[ "$status" -eq 0 ]
|
||||
IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output"
|
||||
[ "$repo_ref" = "ssh://git@example.com:2222/user/repo" ]
|
||||
[ "$owner" = "user" ]
|
||||
[ "$name" = "repo" ]
|
||||
[ "$dirname" = "repo" ] # Should be repo name, not port number
|
||||
}
|
||||
|
||||
@test "parsing should handle SSH URLs with ports and custom directory" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Test SSH URL with port and custom directory
|
||||
run inst_parse_module_spec "ssh://git@example.com:2222/user/repo:my-custom-dir@develop"
|
||||
[ "$status" -eq 0 ]
|
||||
IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output"
|
||||
[ "$repo_ref" = "ssh://git@example.com:2222/user/repo" ]
|
||||
[ "$owner" = "user" ]
|
||||
[ "$name" = "repo" ]
|
||||
[ "$branch" = "develop" ]
|
||||
[ "$dirname" = "my-custom-dir" ]
|
||||
}
|
||||
|
||||
@test "parsing should handle complex URLs with ports, custom dirs, and branches" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Test comprehensive URL with port, custom directory, branch, and commit
|
||||
run inst_parse_module_spec "https://gitlab.example.com:9443/myorg/myrepo.git:custom-name@feature-branch:abc123def"
|
||||
[ "$status" -eq 0 ]
|
||||
IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output"
|
||||
[ "$repo_ref" = "https://gitlab.example.com:9443/myorg/myrepo.git" ]
|
||||
[ "$owner" = "myorg" ]
|
||||
[ "$name" = "myrepo" ]
|
||||
[ "$branch" = "feature-branch" ]
|
||||
[ "$commit" = "abc123def" ]
|
||||
[ "$url" = "https://gitlab.example.com:9443/myorg/myrepo.git" ]
|
||||
[ "$dirname" = "custom-name" ]
|
||||
}
|
||||
|
||||
@test "URL port parsing regression test - ensure ports are not confused with directory names" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# These are the problematic cases that the fix addresses
|
||||
local test_cases=(
|
||||
"https://example.com:8080/repo.git"
|
||||
"https://gitlab.internal:9443/group/project.git"
|
||||
"ssh://git@server.com:2222/owner/repo"
|
||||
"https://git.company.com:8443/team/module.git"
|
||||
)
|
||||
|
||||
for spec in "${test_cases[@]}"; do
|
||||
run inst_parse_module_spec "$spec"
|
||||
[ "$status" -eq 0 ]
|
||||
IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output"
|
||||
|
||||
# Critical: dirname should NEVER be a port number
|
||||
[[ ! "$dirname" =~ ^[0-9]+$ ]] || {
|
||||
echo "FAIL: Port number '$dirname' incorrectly parsed as directory name for spec: $spec"
|
||||
return 1
|
||||
}
|
||||
|
||||
# dirname should be the repository name by default
|
||||
local expected_name
|
||||
if [[ "$spec" =~ /([^/]+)(\.git)?$ ]]; then
|
||||
expected_name="${BASH_REMATCH[1]}"
|
||||
expected_name="${expected_name%.git}"
|
||||
fi
|
||||
[ "$dirname" = "$expected_name" ] || {
|
||||
echo "FAIL: Expected dirname '$expected_name' but got '$dirname' for spec: $spec"
|
||||
return 1
|
||||
}
|
||||
done
|
||||
}
|
||||
|
||||
@test "parsing should handle URL with custom directory but no branch" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
run inst_parse_module_spec "https://github.com/owner/repo.git:my-dir"
|
||||
[ "$status" -eq 0 ]
|
||||
IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output"
|
||||
[ "$repo_ref" = "https://github.com/owner/repo.git" ]
|
||||
[ "$dirname" = "my-dir" ]
|
||||
[ "$branch" = "-" ]
|
||||
}
|
||||
|
||||
@test "modules list should maintain alphabetical order" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
# Add modules in random order
|
||||
inst_mod_list_upsert "zeta/mod-z" "master" "abc"
|
||||
inst_mod_list_upsert "alpha/mod-a" "master" "def"
|
||||
inst_mod_list_upsert "beta/mod-b" "master" "ghi"
|
||||
|
||||
# Read the list and verify alphabetical order
|
||||
local entries=()
|
||||
while read -r repo_ref branch commit; do
|
||||
[[ -z "$repo_ref" ]] && continue
|
||||
entries+=("$repo_ref")
|
||||
done < <(inst_mod_list_read)
|
||||
|
||||
# Should be in alphabetical order by owner/name
|
||||
[ "${entries[0]}" = "alpha/mod-a" ]
|
||||
[ "${entries[1]}" = "beta/mod-b" ]
|
||||
[ "${entries[2]}" = "zeta/mod-z" ]
|
||||
}
|
||||
|
||||
@test "module dispatcher should handle unknown commands gracefully" {
|
||||
cd "$TEST_DIR"
|
||||
source "$TEST_DIR/apps/installer/includes/includes.sh"
|
||||
|
||||
run inst_module "unknown-command"
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" =~ "Unknown module command" ]]
|
||||
}
|
||||
@@ -305,6 +305,31 @@ Services support two restart policies:
|
||||
./service-manager.sh delete auth
|
||||
```
|
||||
|
||||
#### Health and Console Commands
|
||||
|
||||
Use these commands to programmatically check service health and interact with the console (used by CI workflows):
|
||||
|
||||
```bash
|
||||
# Check if service is currently running (exit 0 if running)
|
||||
./service-manager.sh is-running world
|
||||
|
||||
# Print current uptime in seconds (fails if not running)
|
||||
./service-manager.sh uptime-seconds world
|
||||
|
||||
# Wait until uptime >= 10s (optional timeout 240s)
|
||||
./service-manager.sh wait-uptime world 10 240
|
||||
|
||||
# Send a console command (uses pm2 send or tmux/screen)
|
||||
./service-manager.sh send world "server info"
|
||||
|
||||
# Show provider, configs and run-engine settings
|
||||
./service-manager.sh show-config world
|
||||
```
|
||||
|
||||
Notes:
|
||||
- For `send`, PM2 provider uses `pm2 send` with the process ID; systemd provider requires a session manager (tmux/screen). If no attachable session is configured, the command fails.
|
||||
- `wait-uptime` fails with a non-zero exit code if the service does not reach the requested uptime within the timeout window.
|
||||
|
||||
#### Service Configuration
|
||||
```bash
|
||||
# Update service settings
|
||||
@@ -624,4 +649,3 @@ sudo npm install -g pm2
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -279,6 +279,11 @@ function print_help() {
|
||||
echo " $base_name start|stop|restart|status <service-name>"
|
||||
echo " $base_name logs <service-name> [--follow]"
|
||||
echo " $base_name attach <service-name>"
|
||||
echo " $base_name is-running <service-name> # exit 0 if running, 1 otherwise"
|
||||
echo " $base_name uptime-seconds <service-name> # print uptime in seconds (fails if not running)"
|
||||
echo " $base_name wait-uptime <service> <sec> [t] # wait until uptime >= seconds (timeout t, default 120)"
|
||||
echo " $base_name send <service-name> <command...> # send console command to service"
|
||||
echo " $base_name show-config <service-name> # print current service + run-engine config"
|
||||
echo " $base_name edit-config <service-name>"
|
||||
echo ""
|
||||
echo "Providers:"
|
||||
@@ -735,7 +740,7 @@ EOF
|
||||
systemctl --user enable "$service_name.service"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Systemd service '$service_name' created successfully${NC}"
|
||||
echo -e "${GREEN}Systemd service '$service_name' created successfully with session manager '$session_manager'${NC}"
|
||||
|
||||
# Add to registry
|
||||
add_service_to_registry "$service_name" "systemd" "service" "$command" "" "$systemd_type" "$restart_policy" "$session_manager" "$gdb_enabled" "" "$server_config"
|
||||
@@ -1473,6 +1478,253 @@ function attach_to_service() {
|
||||
fi
|
||||
}
|
||||
|
||||
#########################################
|
||||
# Runtime helpers: status / send / show #
|
||||
#########################################
|
||||
|
||||
function service_is_running() {
|
||||
local service_name="$1"
|
||||
|
||||
local service_info=$(get_service_info "$service_name")
|
||||
if [ -z "$service_info" ]; then
|
||||
echo -e "${RED}Error: Service '$service_name' not found${NC}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local provider=$(echo "$service_info" | jq -r '.provider')
|
||||
|
||||
if [ "$provider" = "pm2" ]; then
|
||||
# pm2 jlist -> JSON array with .name and .pm2_env.status
|
||||
if pm2 jlist | jq -e ".[] | select(.name==\"$service_name\" and .pm2_env.status==\"online\")" >/dev/null; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
elif [ "$provider" = "systemd" ]; then
|
||||
# Check user service first, then system
|
||||
if systemctl --user is-active --quiet "$service_name.service" 2>/dev/null; then
|
||||
return 0
|
||||
elif systemctl is-active --quiet "$service_name.service" 2>/dev/null; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
function service_send_command() {
|
||||
local service_name="$1"; shift || true
|
||||
local cmd_str="$*"
|
||||
if [ -z "$service_name" ] || [ -z "$cmd_str" ]; then
|
||||
echo -e "${RED}Error: send requires <service-name> and <command>${NC}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local service_info=$(get_service_info "$service_name")
|
||||
if [ -z "$service_info" ]; then
|
||||
echo -e "${RED}Error: Service '$service_name' not found${NC}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local provider=$(echo "$service_info" | jq -r '.provider')
|
||||
local config_file="$CONFIG_DIR/$service_name.conf"
|
||||
|
||||
if [ ! -f "$config_file" ]; then
|
||||
echo -e "${RED}Error: Service configuration file not found: $config_file${NC}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Load run-engine config path
|
||||
# shellcheck source=/dev/null
|
||||
source "$config_file"
|
||||
if [ -z "${RUN_ENGINE_CONFIG_FILE:-}" ] || [ ! -f "$RUN_ENGINE_CONFIG_FILE" ]; then
|
||||
echo -e "${RED}Error: Run-engine configuration file not found for $service_name${NC}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
if ! source "$RUN_ENGINE_CONFIG_FILE"; then
|
||||
echo -e "${RED}Error: Failed to source run-engine configuration file: $RUN_ENGINE_CONFIG_FILE${NC}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local session_manager="${SESSION_MANAGER:-auto}"
|
||||
local session_name="${SESSION_NAME:-$service_name}"
|
||||
|
||||
if [ "$provider" = "pm2" ]; then
|
||||
# Use pm2 send (requires pm2 >= 5)
|
||||
local pm2_id_json
|
||||
pm2_id_json=$(pm2 id "$service_name" 2>/dev/null || true)
|
||||
local numeric_id
|
||||
numeric_id=$(echo "$pm2_id_json" | jq -r '.[0] // empty')
|
||||
if [ -z "$numeric_id" ]; then
|
||||
echo -e "${RED}Error: PM2 process '$service_name' not found${NC}" >&2
|
||||
return 1
|
||||
fi
|
||||
echo -e "${YELLOW}Sending to PM2 process $service_name (ID: $numeric_id): $cmd_str${NC}"
|
||||
pm2 send "$numeric_id" "$cmd_str" ENTER
|
||||
return $?
|
||||
fi
|
||||
|
||||
# systemd provider: need a session manager to interact with the console
|
||||
case "$session_manager" in
|
||||
tmux|auto)
|
||||
if command -v tmux >/dev/null 2>&1 && tmux has-session -t "$session_name" 2>/dev/null; then
|
||||
echo -e "${YELLOW}Sending to tmux session $session_name: $cmd_str${NC}"
|
||||
tmux send-keys -t "$session_name" "$cmd_str" C-m
|
||||
return $?
|
||||
elif [ "$session_manager" = "tmux" ]; then
|
||||
echo -e "${RED}Error: tmux session '$session_name' not available${NC}" >&2
|
||||
return 1
|
||||
fi
|
||||
;;&
|
||||
screen|auto)
|
||||
if command -v screen >/dev/null 2>&1; then
|
||||
echo -e "${YELLOW}Sending to screen session $session_name: $cmd_str${NC}"
|
||||
screen -S "$session_name" -X stuff "$cmd_str\n"
|
||||
return $?
|
||||
elif [ "$session_manager" = "screen" ]; then
|
||||
echo -e "${RED}Error: screen not installed${NC}" >&2
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
none|*)
|
||||
echo -e "${RED}Error: No session manager configured (SESSION_MANAGER=$session_manager). Cannot send command.${NC}" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo -e "${RED}Error: Unable to find usable session (tmux/screen) to send command.${NC}" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
function show_config() {
|
||||
local service_name="$1"
|
||||
if [ -z "$service_name" ]; then
|
||||
echo -e "${RED}Error: Service name required for show-config${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local service_info=$(get_service_info "$service_name")
|
||||
if [ -z "$service_info" ]; then
|
||||
echo -e "${RED}Error: Service '$service_name' not found${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local provider=$(echo "$service_info" | jq -r '.provider')
|
||||
local cfg_file="$CONFIG_DIR/$service_name.conf"
|
||||
echo -e "${BLUE}Service: $service_name${NC}"
|
||||
echo "Provider: $provider"
|
||||
echo "Config file: $cfg_file"
|
||||
if [ -f "$cfg_file" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
source "$cfg_file"
|
||||
echo "RUN_ENGINE_CONFIG_FILE: ${RUN_ENGINE_CONFIG_FILE:-<none>}"
|
||||
if [ -n "${RUN_ENGINE_CONFIG_FILE:-}" ] && [ -f "$RUN_ENGINE_CONFIG_FILE" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
source "$RUN_ENGINE_CONFIG_FILE"
|
||||
echo "Session manager: ${SESSION_MANAGER:-}"
|
||||
echo "Session name: ${SESSION_NAME:-}"
|
||||
echo "BINPATH: ${BINPATH:-}"
|
||||
echo "SERVERBIN: ${SERVERBIN:-}"
|
||||
echo "CONFIG: ${CONFIG:-}"
|
||||
echo "RESTART_POLICY: ${RESTART_POLICY:-}"
|
||||
fi
|
||||
else
|
||||
echo "Config file not found"
|
||||
fi
|
||||
}
|
||||
|
||||
# Return uptime in seconds for a service (echo integer), non-zero exit if not running
|
||||
function service_uptime_seconds() {
|
||||
local service_name="$1"
|
||||
local service_info=$(get_service_info "$service_name")
|
||||
if [ -z "$service_info" ]; then
|
||||
echo -e "${RED}Error: Service '$service_name' not found${NC}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local provider=$(echo "$service_info" | jq -r '.provider')
|
||||
|
||||
if [ "$provider" = "pm2" ]; then
|
||||
check_pm2 || return 1
|
||||
local info_json
|
||||
info_json=$(pm2 jlist 2>/dev/null)
|
||||
local pm_uptime_ms
|
||||
pm_uptime_ms=$(echo "$info_json" | jq -r ".[] | select(.name==\"$service_name\").pm2_env.pm_uptime // empty")
|
||||
local status
|
||||
status=$(echo "$info_json" | jq -r ".[] | select(.name==\"$service_name\").pm2_env.status // empty")
|
||||
if [ -z "$pm_uptime_ms" ] || [ "$status" != "online" ]; then
|
||||
return 1
|
||||
fi
|
||||
# Current time in ms (fallback to seconds*1000 if %N unsupported)
|
||||
local now_ms
|
||||
if date +%s%N >/dev/null 2>&1; then
|
||||
now_ms=$(( $(date +%s%N) / 1000000 ))
|
||||
else
|
||||
now_ms=$(( $(date +%s) * 1000 ))
|
||||
fi
|
||||
local diff_ms=$(( now_ms - pm_uptime_ms ))
|
||||
[ "$diff_ms" -lt 0 ] && diff_ms=0
|
||||
echo $(( diff_ms / 1000 ))
|
||||
return 0
|
||||
elif [ "$provider" = "systemd" ]; then
|
||||
check_systemd || return 1
|
||||
local systemd_type="--user"
|
||||
[ -f "/etc/systemd/system/$service_name.service" ] && systemd_type="--system"
|
||||
|
||||
# Get ActiveEnterTimestampMonotonic in usec and ActiveState
|
||||
local prop
|
||||
if [ "$systemd_type" = "--system" ]; then
|
||||
prop=$(systemctl show "$service_name.service" --property=ActiveEnterTimestampMonotonic,ActiveState 2>/dev/null)
|
||||
else
|
||||
prop=$(systemctl --user show "$service_name.service" --property=ActiveEnterTimestampMonotonic,ActiveState 2>/dev/null)
|
||||
fi
|
||||
local state
|
||||
state=$(echo "$prop" | awk -F= '/^ActiveState=/{print $2}')
|
||||
[ "$state" != "active" ] && return 1
|
||||
local enter_us
|
||||
enter_us=$(echo "$prop" | awk -F= '/^ActiveEnterTimestampMonotonic=/{print $2}')
|
||||
# Current monotonic time in seconds since boot
|
||||
local now_s
|
||||
now_s=$(cut -d' ' -f1 /proc/uptime)
|
||||
# Compute uptime = now_monotonic - enter_monotonic
|
||||
# enter_us may be empty on some systems; fallback to 0
|
||||
enter_us=${enter_us:-0}
|
||||
# Convert now_s to microseconds using awk for precision, then compute diff
|
||||
local diff_s
|
||||
diff_s=$(awk -v now="$now_s" -v enter="$enter_us" 'BEGIN{printf "%d", (now*1000000 - enter)/1000000}')
|
||||
[ "$diff_s" -lt 0 ] && diff_s=0
|
||||
echo "$diff_s"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Wait until service has at least <min_seconds> uptime. Optional timeout seconds (default 120)
|
||||
function wait_service_uptime() {
|
||||
local service_name="$1"
|
||||
local min_seconds="$2"
|
||||
local timeout="${3:-120}"
|
||||
local waited=0
|
||||
|
||||
while [ "$waited" -le "$timeout" ]; do
|
||||
if secs=$(service_uptime_seconds "$service_name" 2>/dev/null); then
|
||||
if [ "$secs" -ge "$min_seconds" ]; then
|
||||
echo -e "${GREEN}Service '$service_name' has reached ${secs}s uptime (required: ${min_seconds}s)${NC}"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
sleep 1
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
echo -e "${RED}Timeout: $service_name did not reach ${min_seconds}s uptime within ${timeout}s${NC}" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
function attach_pm2_process() {
|
||||
local service_name="$1"
|
||||
|
||||
@@ -1629,7 +1881,7 @@ case "${1:-help}" in
|
||||
delete_service "$2"
|
||||
;;
|
||||
list)
|
||||
list_services "$2"
|
||||
list_services "${2:-}"
|
||||
;;
|
||||
restore)
|
||||
restore_missing_services
|
||||
@@ -1670,6 +1922,52 @@ case "${1:-help}" in
|
||||
fi
|
||||
attach_to_service "$2"
|
||||
;;
|
||||
uptime-seconds)
|
||||
if [ $# -lt 2 ]; then
|
||||
echo -e "${RED}Error: Service name required for uptime-seconds command${NC}"
|
||||
print_help
|
||||
exit 1
|
||||
fi
|
||||
service_uptime_seconds "$2"
|
||||
;;
|
||||
wait-uptime)
|
||||
if [ $# -lt 3 ]; then
|
||||
echo -e "${RED}Error: Usage: $0 wait-uptime <service-name> <min-seconds> [timeout]${NC}"
|
||||
print_help
|
||||
exit 1
|
||||
fi
|
||||
wait_service_uptime "$2" "$3" "${4:-120}"
|
||||
;;
|
||||
is-running)
|
||||
if [ $# -lt 2 ]; then
|
||||
echo -e "${RED}Error: Service name required for is-running command${NC}"
|
||||
print_help
|
||||
exit 1
|
||||
fi
|
||||
if service_is_running "$2"; then
|
||||
echo -e "${GREEN}Service '$2' is running${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${YELLOW}Service '$2' is not running${NC}"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
send)
|
||||
if [ $# -lt 3 ]; then
|
||||
echo -e "${RED}Error: Not enough arguments for send command${NC}"
|
||||
print_help
|
||||
exit 1
|
||||
fi
|
||||
service_send_command "$2" "${@:3}"
|
||||
;;
|
||||
show-config)
|
||||
if [ $# -lt 2 ]; then
|
||||
echo -e "${RED}Error: Service name required for show-config command${NC}"
|
||||
print_help
|
||||
exit 1
|
||||
fi
|
||||
show_config "$2"
|
||||
;;
|
||||
help|--help|-h)
|
||||
print_help
|
||||
;;
|
||||
|
||||
@@ -143,6 +143,130 @@ teardown() {
|
||||
[[ "$output" =~ "on-failure|always" ]]
|
||||
}
|
||||
|
||||
@test "service-manager: help lists health and console commands" {
|
||||
run "$SCRIPT_DIR/service-manager.sh" help
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" =~ "is-running <service-name>" ]]
|
||||
[[ "$output" =~ "uptime-seconds <service-name>" ]]
|
||||
[[ "$output" =~ "wait-uptime <service> <sec>" ]]
|
||||
[[ "$output" =~ "send <service-name>" ]]
|
||||
[[ "$output" =~ "show-config <service-name>" ]]
|
||||
}
|
||||
|
||||
@test "service-manager: pm2 uptime and wait-uptime work with mocked pm2" {
|
||||
command -v jq >/dev/null 2>&1 || skip "jq not installed"
|
||||
export AC_SERVICE_CONFIG_DIR="$TEST_DIR/services"
|
||||
mkdir -p "$AC_SERVICE_CONFIG_DIR"
|
||||
# Create registry with pm2 provider service
|
||||
cat > "$AC_SERVICE_CONFIG_DIR/service_registry.json" << 'EOF'
|
||||
[
|
||||
{"name":"test-world","provider":"pm2","type":"service","bin_path":"/bin/worldserver","args":"","systemd_type":"--user","restart_policy":"always"}
|
||||
]
|
||||
EOF
|
||||
# Create minimal service config and run-engine config files required by 'send'
|
||||
echo "RUN_ENGINE_CONFIG_FILE=\"$AC_SERVICE_CONFIG_DIR/test-world-run-engine.conf\"" > "$AC_SERVICE_CONFIG_DIR/test-world.conf"
|
||||
cat > "$AC_SERVICE_CONFIG_DIR/test-world-run-engine.conf" << 'EOF'
|
||||
export SESSION_MANAGER="none"
|
||||
export SESSION_NAME="test-world"
|
||||
EOF
|
||||
# Mock pm2
|
||||
cat > "$TEST_DIR/bin/pm2" << 'EOF'
|
||||
#!/usr/bin/env bash
|
||||
case "$1" in
|
||||
jlist)
|
||||
# Produce a JSON with uptime ~20 seconds
|
||||
if date +%s%N >/dev/null 2>&1; then
|
||||
nowms=$(( $(date +%s%N) / 1000000 ))
|
||||
else
|
||||
nowms=$(( $(date +%s) * 1000 ))
|
||||
fi
|
||||
up=$(( nowms - 20000 ))
|
||||
echo "[{\"name\":\"test-world\",\"pm2_env\":{\"status\":\"online\",\"pm_uptime\":$up}}]"
|
||||
;;
|
||||
id)
|
||||
echo "[1]"
|
||||
;;
|
||||
attach|send|list|describe|logs)
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
chmod +x "$TEST_DIR/bin/pm2"
|
||||
|
||||
run "$SCRIPT_DIR/service-manager.sh" uptime-seconds test-world
|
||||
debug_on_failure
|
||||
[ "$status" -eq 0 ]
|
||||
# Output should be a number >= 10
|
||||
[[ "$output" =~ ^[0-9]+$ ]]
|
||||
[ "$output" -ge 10 ]
|
||||
|
||||
run "$SCRIPT_DIR/service-manager.sh" wait-uptime test-world 10 5
|
||||
debug_on_failure
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "service-manager: send works under pm2 with mocked pm2" {
|
||||
command -v jq >/dev/null 2>&1 || skip "jq not installed"
|
||||
export AC_SERVICE_CONFIG_DIR="$TEST_DIR/services"
|
||||
mkdir -p "$AC_SERVICE_CONFIG_DIR"
|
||||
# Create registry and config as in previous test
|
||||
cat > "$AC_SERVICE_CONFIG_DIR/service_registry.json" << 'EOF'
|
||||
[
|
||||
{"name":"test-world","provider":"pm2","type":"service","bin_path":"/bin/worldserver","args":"","systemd_type":"--user","restart_policy":"always"}
|
||||
]
|
||||
EOF
|
||||
echo "RUN_ENGINE_CONFIG_FILE=\"$AC_SERVICE_CONFIG_DIR/test-world-run-engine.conf\"" > "$AC_SERVICE_CONFIG_DIR/test-world.conf"
|
||||
cat > "$AC_SERVICE_CONFIG_DIR/test-world-run-engine.conf" << 'EOF'
|
||||
export SESSION_MANAGER="none"
|
||||
export SESSION_NAME="test-world"
|
||||
EOF
|
||||
# pm2 mock
|
||||
cat > "$TEST_DIR/bin/pm2" << 'EOF'
|
||||
#!/usr/bin/env bash
|
||||
case "$1" in
|
||||
jlist)
|
||||
if date +%s%N >/dev/null 2>&1; then
|
||||
nowms=$(( $(date +%s%N) / 1000000 ))
|
||||
else
|
||||
nowms=$(( $(date +%s) * 1000 ))
|
||||
fi
|
||||
up=$(( nowms - 15000 ))
|
||||
echo "[{\"name\":\"test-world\",\"pm2_env\":{\"status\":\"online\",\"pm_uptime\":$up}}]"
|
||||
;;
|
||||
id)
|
||||
echo "[1]"
|
||||
;;
|
||||
send)
|
||||
# simulate success
|
||||
exit 0
|
||||
;;
|
||||
attach|list|describe|logs)
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
chmod +x "$TEST_DIR/bin/pm2"
|
||||
|
||||
run "$SCRIPT_DIR/service-manager.sh" send test-world "server info"
|
||||
debug_on_failure
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "service-manager: wait-uptime times out for unknown service" {
|
||||
command -v jq >/dev/null 2>&1 || skip "jq not installed"
|
||||
export AC_SERVICE_CONFIG_DIR="$TEST_DIR/services"
|
||||
mkdir -p "$AC_SERVICE_CONFIG_DIR"
|
||||
echo "[]" > "$AC_SERVICE_CONFIG_DIR/service_registry.json"
|
||||
run "$SCRIPT_DIR/service-manager.sh" wait-uptime unknown 2 1
|
||||
[ "$status" -ne 0 ]
|
||||
}
|
||||
|
||||
# ===== EXAMPLE SCRIPTS TESTS =====
|
||||
|
||||
@test "examples: restarter-world should show configuration error" {
|
||||
|
||||
Reference in New Issue
Block a user