Test the changes that have been made

This commit is contained in:
Bishoy Ehab
2025-10-18 22:53:52 +03:00
parent 0facd08fa9
commit 1fd328f90a
13 changed files with 383 additions and 260 deletions
+66
View File
@@ -0,0 +1,66 @@
name: Smoke Test - exp-update
on:
push:
paths:
- 'install.sh'
- 'sdata/step/exp-update.sh'
- 'sdata/lib/options-exp-update.sh'
pull_request:
paths:
- 'install.sh'
- 'sdata/step/exp-update.sh'
- 'sdata/lib/options-exp-update.sh'
workflow_dispatch:
jobs:
smoke-test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Make install.sh executable
run: chmod +x install.sh
- name: Run smoke test
run: |
echo "Running: ./install.sh exp-update --non-interactive --skip-notice --dry-run -v"
# Capture output and exit code
OUTPUT=$(./install.sh exp-update --non-interactive --skip-notice --dry-run -v 2>&1)
EXIT_CODE=$?
echo "Exit code: $EXIT_CODE"
echo "Output:"
echo "$OUTPUT"
# Check exit code
if [ $EXIT_CODE -ne 0 ]; then
echo "❌ Smoke test failed: Non-zero exit code"
exit 1
fi
# Check for expected strings in output
if ! echo "$OUTPUT" | grep -q "DRY-RUN MODE"; then
echo "❌ Smoke test failed: Missing 'DRY-RUN MODE' in output"
exit 1
fi
if ! echo "$OUTPUT" | grep -q "Detecting Repository Structure"; then
echo "❌ Smoke test failed: Missing 'Detecting Repository Structure' in output"
exit 1
fi
# Check for non-empty Structure line (should contain detected directories)
STRUCTURE_LINE=$(echo "$OUTPUT" | grep "Structure:" | head -1)
if [[ -z "$STRUCTURE_LINE" ]] || [[ "$STRUCTURE_LINE" == "Structure:" ]]; then
echo "❌ Smoke test failed: Structure line is empty or malformed"
echo "Found: $STRUCTURE_LINE"
exit 1
fi
echo "✅ Smoke test passed: All checks successful"
echo "Detected structure: $STRUCTURE_LINE"
+1 -3
View File
@@ -1,8 +1,6 @@
#!/usr/bin/env bash
cd "$(dirname "$0")"
# TODO: Use REPO_ROOT instead of base
# Also, when scripts are sourced they do not need export to inherit vars
export base="$(pwd)"
# Use REPO_ROOT instead of base - when scripts are sourced they do not need export to inherit vars
REPO_ROOT="$(pwd)"
source ./sdata/lib/environment-variables.sh
source ./sdata/lib/functions.sh
+8 -6
View File
@@ -1,6 +1,8 @@
# This is NOT a script for execution, but for loading functions, so NOT need execution permission or shebang.
# NOTE that you NOT need to `cd ..' because the `$0' is NOT this file, but the script file which will source this file.
# shellcheck shell=bash
# The script that use this file should have two lines on its top as follows:
# cd "$(dirname "$0")"
# export base="$(pwd)"
@@ -9,7 +11,7 @@ function try { "$@" || sleep 0; }
function v(){
echo -e "####################################################"
echo -e "${STY_BLUE}[$0]: Next command:${STY_RST}"
echo -e "${STY_GREEN}$@${STY_RST}"
echo -e "${STY_GREEN}$*${STY_RST}"
local execute=true
if $ask;then
while true;do
@@ -29,14 +31,14 @@ function v(){
done
fi
if $execute;then x "$@";else
echo -e "${STY_YELLOW}[$0]: Skipped \"$@\"${STY_RST}"
echo -e "${STY_YELLOW}[$0]: Skipped \"$*\"${STY_RST}"
fi
}
# When use v() for a defined function, use x() INSIDE its definition to catch errors.
function x(){
if "$@";then local cmdstatus=0;else local cmdstatus=1;fi # 0=normal; 1=failed; 2=failed but ignored
while [ $cmdstatus == 1 ] ;do
echo -e "${STY_RED}[$0]: Command \"${STY_GREEN}$@${STY_RED}\" has failed."
echo -e "${STY_RED}[$0]: Command \"${STY_GREEN}$*${STY_RED}\" has failed."
echo -e "You may need to resolve the problem manually BEFORE repeating this command."
echo -e "[Tip] If a certain package is failing to install, try installing it separately in another terminal.${STY_RST}"
echo " r = Repeat this command (DEFAULT)"
@@ -52,9 +54,9 @@ function x(){
esac
done
case $cmdstatus in
0) echo -e "${STY_BLUE}[$0]: Command \"${STY_GREEN}$@${STY_BLUE}\" finished.${STY_RST}";;
1) echo -e "${STY_RED}[$0]: Command \"${STY_GREEN}$@${STY_RED}\" has failed. Exiting...${STY_RST}";exit 1;;
2) echo -e "${STY_RED}[$0]: Command \"${STY_GREEN}$@${STY_RED}\" has failed but ignored by user.${STY_RST}";;
0) echo -e "${STY_BLUE}[$0]: Command \"${STY_GREEN}$*${STY_BLUE}\" finished.${STY_RST}";;
1) echo -e "${STY_RED}[$0]: Command \"${STY_GREEN}$*${STY_RED}\" has failed. Exiting...${STY_RST}";exit 1;;
2) echo -e "${STY_RED}[$0]: Command \"${STY_GREEN}$*${STY_RED}\" has failed but ignored by user.${STY_RST}";;
esac
}
function showfun(){
+19 -2
View File
@@ -1,13 +1,20 @@
# Handle args for subcmd: exp-update
# shellcheck shell=bash
showhelp(){
echo -e "Syntax: $0 exp-update [OPTIONS]...
echo -e "Usage: install.sh exp-update [OPTIONS]...
Experimental updating without full reinstall.
Updates dotfiles by syncing configuration files to home directory.
Options:
-f, --force Force check all files even if no new commits
-p, --packages Enable package checking and building
-n, --dry-run Show what would be done without making changes
-v, --verbose Enable verbose output
-h, --help Show this help message
-s, --skip-notice Skip notice about script being untested
--non-interactive Run without prompting for user input
This script updates your dotfiles by:
1. Auto-detecting repository structure (dots/ prefix or direct)
@@ -15,12 +22,19 @@ This script updates your dotfiles by:
3. Optionally rebuilding packages (if -p flag is used)
4. Syncing configuration files to home directory
5. Updating script permissions
Ignore file patterns support:
- Exact matches (e.g., 'path/to/file')
- Directory patterns (e.g., 'path/to/dir/')
- Wildcards (e.g., '*.log', 'path/*/file')
- Root-relative patterns (e.g., '/.config')
- Substring matching (prefix with '**', e.g., '**temp' matches any path containing 'temp')
"
}
# `man getopt` to see more
para=$(getopt \
-o hfpnv \
-l help,force,packages,dry-run,verbose,skip-notice \
-l help,force,packages,dry-run,verbose,skip-notice,non-interactive \
-n "$0" -- "$@")
[ $? != 0 ] && echo "$0: Error when getopt, please recheck parameters." && exit 1
#####################################################################################
@@ -42,6 +56,7 @@ CHECK_PACKAGES=false
DRY_RUN=false
VERBOSE=false
SKIP_NOTICE=false
NON_INTERACTIVE=false
eval set -- "$para"
while true ; do
@@ -57,6 +72,8 @@ while true ; do
# log_info "Verbose mode enabled"
--skip-notice) SKIP_NOTICE=true;shift;;
# log_warning "Skipping notice about script being untested"
--non-interactive) NON_INTERACTIVE=true;shift;;
# log_info "Non-interactive mode enabled"
## Ending
--) break ;;
+1
View File
@@ -1,4 +1,5 @@
# Handle args for subcmd: install
# shellcheck shell=bash
showhelp(){
echo -e "Syntax: $0 [OPTIONS]...
+4 -1
View File
@@ -1,6 +1,8 @@
# This is NOT a script for execution, but for loading functions, so NOT need execution permission or shebang.
# NOTE that you NOT need to `cd ..' because the `$0' is NOT this file, but the script file which will source this file.
# shellcheck shell=bash
# The script that use this file should have two lines on its top as follows:
# cd "$(dirname "$0")" export base="$(pwd)"
showhelp_global(){
@@ -35,11 +37,12 @@ Subcommand:
Subcommand:
exp-update Using experimental update script.
Options for exp-update:
-u, --update-force Force check all files even if no new commits (update script)
-f, --force Force check all files even if no new commits (update script)
-p, --packages Enable package checking and building (update script)
-n, --dry-run Show what would be done without making changes (update script)
-v, --verbose Enable verbose output (update script)
--skip-notice Skip warning notice (for experimental scripts)
--non-interactive Run without prompting for user input
"
}
+20 -18
View File
@@ -2,6 +2,8 @@
# This is NOT a script for execution, but for loading functions, so NOT need execution permission or shebang.
# NOTE that you NOT need to `cd ..' because the `$0' is NOT this file, but the script file which will source this file.
# shellcheck shell=bash
# This file is provided for any distros, mainly non-Arch(based) distros.
# The script that use this file should have two lines on its top as follows:
@@ -9,8 +11,8 @@
# export base="$(pwd)"
install-agsv1(){
x mkdir -p $base/cache/agsv1
x cd $base/cache/agsv1
x mkdir -p $REPO_ROOT/cache/agsv1
x cd $REPO_ROOT/cache/agsv1
try git init -b main
try git remote add origin https://github.com/Aylur/ags.git
x git pull origin main && git submodule update --init --recursive
@@ -20,12 +22,12 @@ install-agsv1(){
x meson setup build # --reconfigure
x meson install -C build
x sudo mv /usr/local/bin/ags{,v1}
x cd $base
x cd $REPO_ROOT
}
install-Rubik(){
x mkdir -p $base/cache/Rubik
x cd $base/cache/Rubik
x mkdir -p $REPO_ROOT/cache/Rubik
x cd $REPO_ROOT/cache/Rubik
try git init -b main
try git remote add origin https://github.com/googlefonts/rubik.git
x git pull origin main && git submodule update --init --recursive
@@ -35,12 +37,12 @@ install-Rubik(){
x sudo cp OFL.txt /usr/local/share/licenses/ttf-rubik/LICENSE
x fc-cache -fv
x gsettings set org.gnome.desktop.interface font-name 'Rubik 11'
x cd $base
x cd $REPO_ROOT
}
install-Gabarito(){
x mkdir -p $base/cache/Gabarito
x cd $base/cache/Gabarito
x mkdir -p $REPO_ROOT/cache/Gabarito
x cd $REPO_ROOT/cache/Gabarito
try git init -b main
try git remote add origin https://github.com/naipefoundry/gabarito.git
x git pull origin main && git submodule update --init --recursive
@@ -49,12 +51,12 @@ install-Gabarito(){
x sudo mkdir -p /usr/local/share/licenses/ttf-gabarito/
x sudo cp OFL.txt /usr/local/share/licenses/ttf-gabarito/LICENSE
x fc-cache -fv
x cd $base
x cd $REPO_ROOT
}
install-OneUI(){
x mkdir -p $base/cache/OneUI4-Icons
x cd $base/cache/OneUI4-Icons
x mkdir -p $REPO_ROOT/cache/OneUI4-Icons
x cd $REPO_ROOT/cache/OneUI4-Icons
try git init -b main
try git remote add origin https://github.com/end-4/OneUI4-Icons.git
# try git remote add origin https://github.com/mjkim0727/OneUI4-Icons.git
@@ -63,12 +65,12 @@ install-OneUI(){
x sudo cp -r OneUI /usr/local/share/icons
x sudo cp -r OneUI-dark /usr/local/share/icons
x sudo cp -r OneUI-light /usr/local/share/icons
x cd $base
x cd $REPO_ROOT
}
install-bibata(){
x mkdir -p $base/cache/bibata-cursor
x cd $base/cache/bibata-cursor
x mkdir -p $REPO_ROOT/cache/bibata-cursor
x cd $REPO_ROOT/cache/bibata-cursor
name="Bibata-Modern-Classic"
file="$name.tar.xz"
# Use axel because `curl -O` always downloads a file with 0 byte size, idk why
@@ -76,12 +78,12 @@ install-bibata(){
tar -xf $file
x sudo mkdir -p /usr/local/share/icons
x sudo cp -r $name /usr/local/share/icons
x cd $base
x cd $REPO_ROOT
}
install-MicroTeX(){
x mkdir -p $base/cache/MicroTeX
x cd $base/cache/MicroTeX
x mkdir -p $REPO_ROOT/cache/MicroTeX
x cd $REPO_ROOT/cache/MicroTeX
try git init -b master
try git remote add origin https://github.com/NanoMichael/MicroTeX.git
x git pull origin master && git submodule update --init --recursive
@@ -92,7 +94,7 @@ install-MicroTeX(){
x sudo mkdir -p /opt/MicroTeX
x sudo cp ./LaTeX /opt/MicroTeX/
x sudo cp -r ./res /opt/MicroTeX/
x cd $base
x cd $REPO_ROOT
}
install-uv(){
+2
View File
@@ -1,6 +1,8 @@
# This script is meant to be sourced.
# It's not for directly running.
# shellcheck shell=bash
#####################################################################################
printf "${STY_CYAN}[$0]: Hi there! Before we start:${STY_RST}\n"
+2
View File
@@ -1,6 +1,8 @@
# This script is meant to be sourced.
# It's not for directly running.
# shellcheck shell=bash
####################
# Detect distro
# Helpful link(s):
+4 -2
View File
@@ -1,6 +1,8 @@
# This script is meant to be sourced.
# It's not for directly running.
# shellcheck shell=bash
# TODO: https://github.com/end-4/dots-hyprland/issues/2137
function warning_rsync(){
@@ -179,7 +181,7 @@ warn_files_tests+=(/usr/local/share/licenses/ttf-gabarito)
warn_files_tests+=(/usr/local/share/icons/OneUI{,-dark,-light})
warn_files_tests+=(/usr/local/share/icons/Bibata-Modern-Classic)
warn_files_tests+=(/usr/local/bin/{LaTeX,res})
for i in ${warn_files_tests[@]}; do
for i in "${warn_files_tests[@]}"; do
echo $i
test -f $i && warn_files+=($i)
test -d $i && warn_files+=($i)
@@ -223,6 +225,6 @@ if [[ -z "${ILLOGICAL_IMPULSE_VIRTUAL_ENV}" ]]; then
printf "\n${STY_RED}[$0]: \!! Important \!! : Please ensure environment variable ${STY_RST} \$ILLOGICAL_IMPULSE_VIRTUAL_ENV ${STY_RED} is set to proper value (by default \"~/.local/state/quickshell/.venv\"), or Quickshell config will not work. We have already provided this configuration in ~/.config/hypr/hyprland/env.conf, but you need to ensure it is included in hyprland.conf, and also a restart is needed for applying it.${STY_RST}\n"
fi
if [[ ! -z "${warn_files[@]}" ]]; then
if [[ ${#warn_files[@]} -gt 0 ]]; then
printf "\n${STY_RED}[$0]: \!! Important \!! : Please delete ${STY_RST} ${warn_files[*]} ${STY_RED} manually as soon as possible, since we\'re now using AUR package or local PKGBUILD to install them for Arch(based) Linux distros, and they'll take precedence over our installation, or at least take up more space.${STY_RST}\n"
fi
+3 -1
View File
@@ -1,6 +1,8 @@
# This script is meant to be sourced.
# It's not for directly running.
# shellcheck shell=bash
printf 'Hi there!\n'
printf 'This script 1. will uninstall [end-4/dots-hyprland > illogical-impulse] dotfiles\n'
printf ' 2. will try to revert *mostly everything* installed using install.sh, so it'\''s pretty destructive\n'
@@ -40,7 +42,7 @@ starship.toml
thorium-flags.conf
)
for i in ${dirs[@]}
for i in "${dirs[@]}"
do v rm -rf "$XDG_CONFIG_HOME/$i"
done
+187 -192
View File
@@ -1,15 +1,13 @@
#!/usr/bin/env bash
#
# exp-update-tester.sh - Test suite for update.sh
# exp-update-tester.sh - Test suite for update.sh (sourced subcommand)
#
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
TESTS_PASSED=0
@@ -32,22 +30,22 @@ log_fail() {
((TESTS_FAILED++))
}
log_info() {
echo -e "${YELLOW}[INFO]${NC} $1"
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Setup test environment
setup_test_env() {
local temp_dir
temp_dir=$(mktemp -d -t dotfiles-test.XXXXXX)
cd "$temp_dir" || { echo "Failed to cd to test directory"; return 1; }
git init -q
git config user.email "test@example.com"
git config user.name "Test User"
git commit --allow-empty -m "Initial commit" -q
echo "$temp_dir"
}
@@ -63,10 +61,10 @@ cleanup_test_env() {
run_test() {
local test_name="$1"
local test_func="$2"
# Cleanup before test
cleanup_test_env
# Run the test
if $test_func; then
echo "$test_name passed"
@@ -79,18 +77,18 @@ run_test() {
# Test 1: Script exists and is executable
test_script_exists() {
log_test "Checking if update.sh exists and is executable"
if [[ ! -f "update.sh" ]]; then
log_fail "update.sh not found"
log_test "Checking if install.sh exists and is executable"
if [[ ! -f "install.sh" ]]; then
log_fail "install.sh not found"
return 1
fi
if [[ ! -x "update.sh" ]]; then
log_fail "update.sh is not executable"
if [[ ! -x "install.sh" ]]; then
log_fail "install.sh is not executable"
return 1
fi
log_pass "Script exists and is executable"
return 0
}
@@ -98,8 +96,8 @@ test_script_exists() {
# Test 2: Script has no syntax errors
test_syntax() {
log_test "Checking script syntax"
if bash -n update.sh; then
if bash -n install.sh; then
log_pass "No syntax errors found"
return 0
else
@@ -111,8 +109,8 @@ test_syntax() {
# Test 3: Help option works
test_help_option() {
log_test "Testing --help option"
if ./update.sh --help 2>&1 | grep -q "Usage:"; then
if ./install.sh exp-update --help 2>&1 | grep -qiE "(Usage|Options|exp-update)"; then
log_pass "Help option works"
return 0
else
@@ -124,52 +122,39 @@ test_help_option() {
# Test 4: Test repository structure detection (dots/ prefix)
test_dots_structure() {
log_test "Testing dots/ prefix structure detection"
local test_repo
test_repo=$(setup_test_env)
TEST_DIR="$test_repo"
cd "$test_repo" || { log_fail "Failed to cd to test directory"; return 1; }
mkdir -p dots/.config/test-app
mkdir -p dots/.local/bin
echo "test config" > dots/.config/test-app/config.conf
git add .
git commit -m "Add dots structure" -q
cat > test_detection.sh << 'EOF'
cat > test_detection.sh << EOF
#!/bin/bash
# Mock logging and style functions/variables
log_info() { :; }
log_warning() { :; }
log_error() { :; }
log_success() { :; }
log_header() { :; }
log_die() { echo "ERROR: \$1"; exit 1; }
STY_CYAN="" STY_RST="" STY_YELLOW=""
REPO_ROOT="$1"
detect_repo_structure() {
local found_dirs=()
if [[ -d "${REPO_ROOT}/dots/.config" ]]; then
found_dirs+=("dots/.config")
[[ -d "${REPO_ROOT}/dots/.local/bin" ]] && found_dirs+=("dots/.local/bin")
elif [[ -d "${REPO_ROOT}/.config" ]]; then
found_dirs+=(".config")
[[ -d "${REPO_ROOT}/.local/bin" ]] && found_dirs+=(".local/bin")
else
for candidate in "dots/.config" ".config" "dots/.local/bin" ".local/bin"; do
if [[ -d "${REPO_ROOT}/${candidate}" ]]; then
if [[ ! " ${found_dirs[*]} " =~ " ${candidate} " ]]; then
found_dirs+=("${candidate}")
fi
fi
done
fi
if [[ ${#found_dirs[@]} -eq 0 ]]; then
echo "ERROR" >&2
return 1
fi
echo "${found_dirs[@]}"
}
source "$ORIGINAL_DIR/sdata/step/exp-update.sh"
detect_repo_structure
EOF
chmod +x test_detection.sh
result=$(./test_detection.sh "$test_repo")
if [[ "$result" == *"dots/.config"* ]]; then
log_pass "Dots structure detected correctly"
cd "$ORIGINAL_DIR"
@@ -184,52 +169,39 @@ EOF
# Test 5: Test flat structure detection
test_flat_structure() {
log_test "Testing flat structure detection"
local test_repo
test_repo=$(setup_test_env)
TEST_DIR="$test_repo"
cd "$test_repo" || { log_fail "Failed to cd to test directory"; return 1; }
mkdir -p .config/test-app
mkdir -p .local/bin
echo "test config" > .config/test-app/config.conf
git add .
git commit -m "Add flat structure" -q
cat > test_detection.sh << 'EOF'
cat > test_detection.sh << EOF
#!/bin/bash
# Mock logging and style functions/variables
log_info() { :; }
log_warning() { :; }
log_error() { :; }
log_success() { :; }
log_header() { :; }
log_die() { echo "ERROR: \$1"; exit 1; }
STY_CYAN="" STY_RST="" STY_YELLOW=""
REPO_ROOT="$1"
detect_repo_structure() {
local found_dirs=()
if [[ -d "${REPO_ROOT}/dots/.config" ]]; then
found_dirs+=("dots/.config")
[[ -d "${REPO_ROOT}/dots/.local/bin" ]] && found_dirs+=("dots/.local/bin")
elif [[ -d "${REPO_ROOT}/.config" ]]; then
found_dirs+=(".config")
[[ -d "${REPO_ROOT}/.local/bin" ]] && found_dirs+=(".local/bin")
else
for candidate in "dots/.config" ".config" "dots/.local/bin" ".local/bin"; do
if [[ -d "${REPO_ROOT}/${candidate}" ]]; then
if [[ ! " ${found_dirs[*]} " =~ " ${candidate} " ]]; then
found_dirs+=("${candidate}")
fi
fi
done
fi
if [[ ${#found_dirs[@]} -eq 0 ]]; then
echo "ERROR" >&2
return 1
fi
echo "${found_dirs[@]}"
}
source "$ORIGINAL_DIR/sdata/step/exp-update.sh"
detect_repo_structure
EOF
chmod +x test_detection.sh
result=$(./test_detection.sh "$test_repo")
if [[ "$result" == *".config"* ]] && [[ "$result" != *"dots/"* ]]; then
log_pass "Flat structure detected correctly"
cd "$ORIGINAL_DIR"
@@ -283,47 +255,22 @@ EOF
mkdir -p .config
mkdir -p secrets
cat > test_ignore.sh << 'EOF'
cat > test_ignore.sh << EOF
#!/bin/bash
# Mock logging and style functions/variables
log_info() { :; }
log_warning() { :; }
log_error() { :; }
log_success() { :; }
log_header() { :; }
log_die() { echo "ERROR: \$1"; exit 1; }
STY_CYAN="" STY_RST="" STY_YELLOW=""
REPO_ROOT="$1"
UPDATE_IGNORE_FILE="${REPO_ROOT}/.updateignore"
HOME_UPDATE_IGNORE_FILE="/dev/null"
should_ignore() {
local file_path="$1"
local relative_path="${file_path#$HOME/}"
local repo_relative=""
if [[ "$file_path" == "$REPO_ROOT"* ]]; then
repo_relative="${file_path#$REPO_ROOT/}"
fi
for ignore_file in "$UPDATE_IGNORE_FILE" "$HOME_UPDATE_IGNORE_FILE"; do
if [[ -f "$ignore_file" ]]; then
while IFS= read -r pattern || [[ -n "$pattern" ]]; do
[[ -z "$pattern" || "$pattern" =~ ^[[:space:]]*# ]] && continue
pattern=$(echo "$pattern" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
[[ -z "$pattern" ]] && continue
if [[ "$relative_path" == "$pattern" ]] || [[ "$repo_relative" == "$pattern" ]]; then
return 0
fi
if [[ "$relative_path" == $pattern ]] || [[ "$repo_relative" == $pattern ]]; then
return 0
fi
if [[ "$pattern" == */ ]]; then
local dir_pattern="${pattern%/}"
if [[ "$relative_path" == "$dir_pattern"/* ]] || [[ "$repo_relative" == "$dir_pattern"/* ]]; then
return 0
fi
fi
if [[ "$file_path" == *"$pattern"* ]] || [[ "$relative_path" == *"$pattern"* ]]; then
return 0
fi
done <"$ignore_file"
fi
done
return 1
}
# Source the production script to use the real should_ignore function
source "$ORIGINAL_DIR/sdata/step/exp-update.sh"
test_cases=(
"$REPO_ROOT/app.log:0"
@@ -335,7 +282,7 @@ test_cases=(
all_passed=true
for test_case in "${test_cases[@]}"; do
IFS=':' read -r file expected <<< "$test_case"
IFS=":" read -r file expected <<< "$test_case"
mkdir -p "$(dirname "$file")"
touch "$file"
@@ -376,16 +323,22 @@ EOF
# Test 8: Test safe_read security - COMPLETELY NON-INTERACTIVE
test_safe_read_security() {
log_test "Testing safe_read uses secure assignment (printf -v)"
# Check that safe_read uses printf -v and not eval
if grep -A 10 "safe_read()" update.sh | grep -q "printf -v.*varname"; then
log_pass "safe_read uses secure printf -v assignment"
return 0
elif grep -A 10 "safe_read()" update.sh | grep -q "eval.*varname"; then
log_fail "safe_read uses vulnerable eval assignment"
local awk_script='/^safe_read() \{/,/^\}/'
local safe_read_function
safe_read_function=$(awk "$awk_script" "$ORIGINAL_DIR/sdata/step/exp-update.sh")
if [[ -z "$safe_read_function" ]]; then
log_fail "Could not find safe_read function"
return 1
fi
# Check for secure printf -v assignment and absence of eval
if echo "$safe_read_function" | grep -q "printf -v" && ! echo "$safe_read_function" | grep -q "eval"; then
log_pass "safe_read uses secure printf -v assignment and no eval"
return 0
else
log_fail "Cannot determine safe_read assignment method"
log_fail "safe_read does not use secure assignment or contains eval"
return 1
fi
}
@@ -393,25 +346,25 @@ test_safe_read_security() {
# Test 9: Test dry-run mode
test_dry_run() {
log_test "Testing dry-run mode"
local test_repo
test_repo=$(setup_test_env)
TEST_DIR="$test_repo"
cd "$test_repo" || { log_fail "Failed to cd to test directory"; return 1; }
mkdir -p dots/.config/test-app
echo "repo config" > dots/.config/test-app/config.conf
# Copy necessary files for install.sh to run
cp "$ORIGINAL_DIR/install.sh" .
cp -r "$ORIGINAL_DIR/sdata" .
cp -r "$ORIGINAL_DIR/dots" .
chmod +x install.sh
git add .
git commit -m "Add test config" -q
cp "$ORIGINAL_DIR/update.sh" .
chmod +x update.sh
# Use printf to pipe responses automatically
printf "y\ny\n" | ./update.sh -n --skip-notice 2>&1 | tee dry_run_output.txt
# Use non-interactive mode and check for DRY-RUN marker
./install.sh exp-update -n --skip-notice --non-interactive 2>&1 | tee dry_run_output.txt
if grep -q "DRY-RUN" dry_run_output.txt; then
log_pass "Dry-run mode detected in output"
else
@@ -419,7 +372,7 @@ test_dry_run() {
cd "$ORIGINAL_DIR"
return 1
fi
if [[ ! -f "${HOME}/.config/test-app/config.conf" ]]; then
log_pass "No files created in home during dry-run"
else
@@ -428,7 +381,7 @@ test_dry_run() {
cd "$ORIGINAL_DIR"
return 1
fi
cd "$ORIGINAL_DIR"
return 0
}
@@ -436,20 +389,20 @@ test_dry_run() {
# Test 10: Test command-line flags
test_flags() {
log_test "Testing command-line flags"
# Only test non-interactive flags
local flags=("-h" "--help")
local all_passed=true
for flag in "${flags[@]}"; do
if ./update.sh "$flag" 2>&1 | grep -q -E "(Usage|help)"; then
log_info "$flag recognized"
if ./install.sh exp-update "$flag" 2>&1 | grep -qiE "(Usage|Options|exp-update)"; then
log_test "$flag recognized"
else
log_info "$flag not recognized"
log_test "$flag not recognized"
all_passed=false
fi
done
if [[ "$all_passed" == true ]]; then
log_pass "Help flags recognized correctly"
return 0
@@ -464,11 +417,11 @@ test_shellcheck() {
log_test "Running shellcheck (if available)"
if ! command -v shellcheck &>/dev/null; then
log_info "shellcheck not found, skipping static analysis"
log_test "shellcheck not found, skipping static analysis"
return 0
fi
if shellcheck -e SC1090,SC1091,SC2148,SC2034,SC2155,SC2164 update.sh; then
if shellcheck -e SC1090,SC1091,SC2148,SC2034,SC2155,SC2164 install.sh; then
log_pass "shellcheck passed"
return 0
else
@@ -477,45 +430,87 @@ test_shellcheck() {
fi
}
# Test 12: Test fresh clone scenario
test_fresh_clone() {
log_test "Testing fresh clone scenario"
# Test 13: Test ** substring ignore patterns
test_substring_ignore_patterns() {
log_test "Testing ** substring ignore pattern matching"
local test_repo
test_repo=$(setup_test_env)
TEST_DIR="$test_repo"
cd "$test_repo" || { log_fail "Failed to cd to test directory"; return 1; }
mkdir -p .config/test-app
echo "config" > .config/test-app/settings.conf
cat > test_fresh_clone.sh << 'EOF'
#!/bin/bash
has_new_commits() {
if git rev-parse --verify HEAD@{1} &>/dev/null; then
[[ "$(git rev-parse HEAD)" != "$(git rev-parse HEAD@{1})" ]]
else
return 0
fi
}
if has_new_commits; then
cd "$test_repo" || { log_fail "Failed to cd to test directory"; return 1; }
cat > .updateignore << 'EOF'
**temp**
**backup**
**test**
EOF
mkdir -p .config/test-app
mkdir -p temp-backup-dir
mkdir -p .local/share/test-temp
mkdir -p .config/temp-file
cat > test_substring_ignore.sh << EOF
#!/bin/bash
# Mock logging and style functions/variables
log_info() { :; }
log_warning() { :; }
log_error() { :; }
log_success() { :; }
log_header() { :; }
log_die() { echo "ERROR: \$1"; exit 1; }
STY_CYAN="" STY_RST="" STY_YELLOW=""
REPO_ROOT="$1"
UPDATE_IGNORE_FILE="${REPO_ROOT}/.updateignore"
HOME_UPDATE_IGNORE_FILE="/dev/null"
# Source the production script to use the real should_ignore function
source "$ORIGINAL_DIR/sdata/step/exp-update.sh"
test_cases=(
"$REPO_ROOT/temp-backup-dir/file:0"
"$REPO_ROOT/.config/test-app/temp.conf:0"
"$REPO_ROOT/.local/share/test-temp/data:0"
"$REPO_ROOT/.config/temp-file/config:0"
"$REPO_ROOT/normal-config:1"
)
all_passed=true
for test_case in "${test_cases[@]}"; do
IFS=":" read -r file expected <<< "$test_case"
mkdir -p "$(dirname "$file")"
touch "$file"
if should_ignore "$file"; then
result=0
else
result=1
fi
if [[ $result -ne $expected ]]; then
echo "FAIL: $file (expected: $expected, got: $result)"
all_passed=false
fi
done
if [[ "$all_passed" == true ]]; then
echo "PASS"
else
echo "FAIL"
fi
EOF
chmod +x test_fresh_clone.sh
result=$(./test_fresh_clone.sh)
chmod +x test_substring_ignore.sh
result=$(./test_substring_ignore.sh "$test_repo")
if [[ "$result" == "PASS" ]]; then
log_pass "Fresh clone scenario handled correctly"
log_pass "** substring ignore patterns work correctly"
cd "$ORIGINAL_DIR"
return 0
else
log_fail "Fresh clone scenario not handled properly"
log_fail "** substring ignore patterns failed"
echo "$result"
cd "$ORIGINAL_DIR"
return 1
fi
@@ -524,32 +519,32 @@ EOF
# Main test runner
main() {
echo -e "${BLUE}================================${NC}"
echo -e "${BLUE} Update.sh Test Suite${NC}"
echo -e "${BLUE} Update.sh Test Suite (Sourced Subcommand)${NC}"
echo -e "${BLUE}================================${NC}\n"
if [[ ! -f "update.sh" ]]; then
log_error "Please run this test from the directory containing update.sh"
if [[ ! -f "install.sh" ]]; then
log_error "Please run this test from the directory containing install.sh"
exit 1
fi
chmod +x update.sh 2>/dev/null || true
chmod +x install.sh 2>/dev/null || true
# Define tests
tests=(
"test_script_exists"
"test_syntax"
"test_syntax"
"test_help_option"
"test_dots_structure"
"test_flat_structure"
"test_dots_mapping"
"test_ignore_patterns"
"test_substring_ignore_patterns"
"test_safe_read_security"
"test_dry_run"
"test_flags"
"test_shellcheck"
"test_fresh_clone"
)
# Run tests
for test in "${tests[@]}"; do
if $test; then
@@ -559,7 +554,7 @@ main() {
fi
echo
done
# Summary
echo -e "${BLUE}================================${NC}"
echo -e "${BLUE} Test Summary${NC}"
@@ -567,7 +562,7 @@ main() {
echo -e "${GREEN}Passed: $TESTS_PASSED${NC}"
echo -e "${RED}Failed: $TESTS_FAILED${NC}"
echo -e "${BLUE}Total: ${#tests[@]}${NC}\n"
if [[ $TESTS_FAILED -eq 0 ]]; then
echo -e "${GREEN}All tests passed! 🎉${NC}\n"
exit 0
@@ -581,7 +576,7 @@ main() {
cleanup() {
echo "Cleaning up test files..."
cleanup_test_env
rm -f test_detection.sh test_ignore.sh test_safe_read.sh test_fresh_clone.sh dry_run_output.txt 2>/dev/null || true
rm -f test_detection.sh test_ignore.sh test_safe_read.sh test_fresh_clone.sh test_substring_ignore.sh dry_run_output.txt 2>/dev/null || true
rm -rf "${HOME}/.config/test-app" 2>/dev/null || true
}
Regular → Executable
+66 -35
View File
@@ -1,6 +1,8 @@
# This script is meant to be sourced.
# It's not for directly running.
# shellcheck shell=bash
#####################################################################################
#
# update.sh - Enhanced dotfiles update script
@@ -10,7 +12,12 @@
# - Pull latest commits from remote
# - Rebuild packages if PKGBUILD files changed (user choice)
# - Handle config file conflicts with user choices
# - Respect .updateignore file for exclusions
# - Respect .updateignore file for exclusions with flexible pattern matching:
# - Exact matches (e.g., "path/to/file")
# - Directory patterns (e.g., "path/to/dir/")
# - Wildcards (e.g., "*.log", "path/*/file")
# - Root-relative patterns (e.g., "/.config")
# - Substring matching (prefix with "**", e.g., "**temp" matches any path containing "temp")
#
set -euo pipefail
@@ -47,7 +54,7 @@ detect_repo_structure() {
[[ -d "${REPO_ROOT}/.local/share" ]] && found_dirs+=(".local/share")
else
# Manual detection of common directories
for candidate in "dots/.config" ".config" "config" "dots/.local/bin" ".local/bin" "dots/.local/share" ".local/share"; do
for candidate in "dots/.config" ".config" "dots/.local/bin" ".local/bin" "dots/.local/share" ".local/share"; do
if [[ -d "${REPO_ROOT}/${candidate}" ]]; then
# Avoid duplicates
if [[ ! " ${found_dirs[*]} " =~ " ${candidate} " ]]; then
@@ -94,8 +101,6 @@ safe_read() {
fi
fi
}
# Function to check if a file should be ignored
should_ignore() {
local file_path="$1"
local relative_path="${file_path#$HOME/}"
@@ -161,9 +166,10 @@ should_ignore() {
done
fi
# Simple substring matching (for backward compatibility)
if [[ ! "$should_skip" == true ]]; then
if [[ "$file_path" == *"$pattern"* ]] || [[ "$relative_path" == *"$pattern"* ]]; then
# Substring matching (only if pattern starts with '**')
if [[ ! "$should_skip" == true && "$pattern" == \*\** ]]; then
local substring_pattern="${pattern#\*\*}" # Remove the leading '**'
if [[ -n "$substring_pattern" && ("$file_path" == *"$substring_pattern"* || "$relative_path" == *"$substring_pattern"*) ]]; then
should_skip=true
fi
fi
@@ -417,7 +423,6 @@ list_packages() {
build_packages() {
local build_mode="$1"
local packages_to_build=()
local rebuilt_packages=0
case "$build_mode" in
"changed")
@@ -474,7 +479,6 @@ build_packages() {
log_info "Package building cancelled by user"
return
fi
for pkg_name in "${packages_to_build[@]}"; do
pkg_dir="${ARCH_PACKAGES_DIR}/${pkg_name}"
@@ -490,7 +494,10 @@ build_packages() {
continue
fi
cd "$pkg_dir" || continue
cd "$pkg_dir" || {
log_error "Failed to change to package directory: $pkg_dir"
continue
}
if makepkg -si --noconfirm; then
log_success "Successfully built and installed $pkg_name"
@@ -655,9 +662,20 @@ rebuilt_packages=0
if [[ "$CHECK_PACKAGES" == true ]]; then
log_header "Package Management"
if [[ ! -d "$ARCH_PACKAGES_DIR" ]]; then
log_warning "No packages directory found (tried: dist-arch, arch-packages, sdist/arch). Skipping package management."
# Check if required Arch Linux tools are available
if ! command -v pacman &>/dev/null || ! command -v makepkg &>/dev/null; then
log_warning "Arch Linux package management tools (pacman/makepkg) not found."
log_warning "Skipping package management as this appears to be a non-Arch Linux system."
log_warning "Use -p/--packages flag only on Arch Linux systems."
PKG_TOOLS_AVAILABLE=false
else
PKG_TOOLS_AVAILABLE=true
fi
if [[ "$PKG_TOOLS_AVAILABLE" == true ]]; then
if [[ ! -d "$ARCH_PACKAGES_DIR" ]]; then
log_warning "No packages directory found (tried: dist-arch, arch-packages, sdist/arch). Skipping package management."
else
changed_pkgbuilds=()
for pkg_dir in "$ARCH_PACKAGES_DIR"/*/; do
if [[ -f "${pkg_dir}/PKGBUILD" ]]; then
@@ -678,7 +696,19 @@ if [[ "$CHECK_PACKAGES" == true ]]; then
echo "4) Skip package building"
echo
if safe_read "Choose an option (1-4): " pkg_choice "1"; then
if [[ "$NON_INTERACTIVE" == true ]]; then
pkg_choice="1"
log_info "Non-interactive mode: Using default package option: $pkg_choice"
elif safe_read "Choose an option (1-4): " pkg_choice "1"; then
if [[ "$VERBOSE" == true ]]; then
log_info "User selected package option: $pkg_choice"
fi
else
log_warning "Failed to read input. Skipping package building."
pkg_choice=""
fi
if [[ -n "$pkg_choice" ]]; then
case $pkg_choice in
1) build_packages "changed" ;;
2)
@@ -689,14 +719,23 @@ if [[ "$CHECK_PACKAGES" == true ]]; then
3) build_packages "all" ;;
4 | *) log_info "Skipping package building" ;;
esac
else
log_warning "Failed to read input. Skipping package building."
fi
else
log_info "No PKGBUILDs have changed since last update."
echo
if safe_read "Do you want to check and build packages anyway? (y/N): " check_anyway "N"; then
if [[ "$check_anyway" =~ ^[Yy]$ ]]; then
if [[ "$NON_INTERACTIVE" == true ]]; then
check_anyway="N"
log_info "Non-interactive mode: Using default for check packages anyway: $check_anyway"
elif safe_read "Do you want to check and build packages anyway? (y/N): " check_anyway "N"; then
if [[ "$VERBOSE" == true ]]; then
log_info "User chose to check packages anyway: $check_anyway"
fi
else
log_warning "Failed to read input. Skipping package management."
check_anyway=""
fi
if [[ -n "$check_anyway" && "$check_anyway" =~ ^[Yy]$ ]]; then
if list_packages; then
echo
echo "Package build options:"
@@ -717,26 +756,11 @@ if [[ "$CHECK_PACKAGES" == true ]]; then
else
log_info "Skipping package management"
fi
else
log_info "Skipping package management"
fi
fi
fi
else
log_header "Package Management"
log_info "Package checking disabled. Use -p or --packages flag to enable package management."
if [[ -d "$ARCH_PACKAGES_DIR" ]]; then
changed_count=0
for pkg_dir in "$ARCH_PACKAGES_DIR"/*/; do
if [[ -f "${pkg_dir}/PKGBUILD" ]] && check_pkgbuild_changed "$pkg_dir"; then
((changed_count++))
fi
done
if [[ $changed_count -gt 0 ]]; then
log_warning "Note: $changed_count package(s) have changed PKGBUILDs. Use -p flag to manage packages."
fi
else
log_header "Package Management"
log_info "Package checking disabled. Use -p or --packages flag to enable package management."
fi
fi
@@ -794,9 +818,16 @@ if [[ "$process_files" == true ]]; then
home_file="${home_dir_path}/${rel_path}"
if should_ignore "$home_file"; then
if [[ "$VERBOSE" == true ]]; then
log_info "Ignored: $rel_path (matches ignore pattern)"
fi
continue
fi
if [[ "$VERBOSE" == true ]]; then
log_info "Processing: $rel_path"
fi
((files_processed++))
if [[ "$DRY_RUN" != true ]]; then