Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/test-shell.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Shell Integration Tests

on:
push:
paths:
- 'src/**/*.sh'
- 'static/**/*.sh'
- 'test/shell-tests/**'
pull_request:
paths:
- 'src/**/*.sh'
- 'static/**/*.sh'
- 'test/shell-tests/**'

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build test image with cache
uses: docker/build-push-action@v5
with:
context: test/shell-tests
load: true
tags: onion-shell-test:latest
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Run shell tests
run: make test-shell
30 changes: 28 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ include ./src/common/commands.mk

###########################################################

.PHONY: all version core apps external release clean deepclean git-clean with-toolchain patch lib test
.PHONY: all version core apps external release clean deepclean git-clean with-toolchain patch lib test test-shell test-shell-debug test-all

all: dist

Expand Down Expand Up @@ -227,7 +227,8 @@ release: dist
clean:
@$(ECHO) $(PRINT_RECIPE)
@rm -rf $(BUILD_DIR) $(BUILD_TEST_DIR) $(ROOT_DIR)/dist $(TEMP_DIR)/configs
@rm -f $(CACHE)/.setup
@rm -f $(CACHE)/.setup $(CACHE)/.shell-test-image
@docker rmi onion-shell-test:latest 2>/dev/null || true
@find include src -type f -name *.o -exec rm -f {} \;

deepclean: clean
Expand Down Expand Up @@ -279,3 +280,28 @@ static-analysis: external-libs

format:
@find ./src -regex '.*\.\(c\|h\|cpp\|hpp\)' -exec clang-format -style=file -i {} \;

# Shell script testing (Docker + BATS)
SHELL_TEST_IMAGE := onion-shell-test:latest
SHELL_TEST_DIR := $(TEST_SRC_DIR)/shell-tests

$(CACHE)/.shell-test-image: $(SHELL_TEST_DIR)/Dockerfile
docker build -t $(SHELL_TEST_IMAGE) $(SHELL_TEST_DIR)
@mkdir -p $(CACHE)
$(createfile) $(CACHE)/.shell-test-image

test-shell: $(CACHE)/.shell-test-image
@$(ECHO) $(COLOR_BLUE)"Running shell tests..."$(COLOR_NORMAL)
docker run --rm \
-v "$(ROOT_DIR)":/root/workspace:ro \
$(SHELL_TEST_IMAGE) \
-r /root/workspace/test/shell-tests/

test-shell-debug: $(CACHE)/.shell-test-image
@$(ECHO) $(COLOR_BLUE)"Starting interactive debug session..."$(COLOR_NORMAL)
docker run -it --rm \
-v "$(ROOT_DIR)":/root/workspace \
--entrypoint /bin/bash \
$(SHELL_TEST_IMAGE)

test-all: test test-shell
22 changes: 22 additions & 0 deletions test/shell-tests/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM alpine:3.18

# Install BATS and shell dependencies
RUN apk add --no-cache bash coreutils findutils grep sed unzip git jq 7zip \
&& apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/testing bats

# Install BATS helper libraries
RUN git clone --depth 1 https://github.com/bats-core/bats-support /opt/bats-support && \
git clone --depth 1 https://github.com/bats-core/bats-assert /opt/bats-assert && \
git clone --depth 1 https://github.com/bats-core/bats-file /opt/bats-file

# Create mock Miyoo Mini filesystem
RUN mkdir -p /mnt/SDCARD/.tmp_update/bin \
/mnt/SDCARD/.tmp_update/config \
/mnt/SDCARD/App/PackageManager/data \
/mnt/SDCARD/Emu \
/mnt/SDCARD/RApp \
/mnt/SDCARD/Saves/CurrentProfile \
/mnt/SDCARD/RetroArch/.retroarch

WORKDIR /root/workspace
ENTRYPOINT ["bats"]
30 changes: 30 additions & 0 deletions test/shell-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Shell Tests

Integration tests using Docker + BATS.

```bash
make test-shell # Run all tests
make test-shell-debug # Interactive debug
```

## Adding Tests

1. Create `<name>.bats` in appropriate folder (`package_manager/`, `packages/`, `theme_switcher/`)
2. Load helpers and use `setup()`:
```bash
load '../../support/test_helper'
load '../../support/mocks'

setup() {
cleanup_test_data
mock_system_commands
}
```
3. Run scripts with `run sh "$PROJECT/path/to/script.sh" args`

## Available Mocks

- `mock_system_commands` - No-ops `sync`, `sleep`; logs `reboot`, `poweroff`
- `mock_date [timestamp]` - Returns fixed timestamp
- `mock_md5sum [hash]` - Returns fixed hash
- `mock_binary <name>` - Creates logging stub
53 changes: 53 additions & 0 deletions test/shell-tests/package_manager/pacman_install.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env bats

load '../support/test_helper'
load '../support/mocks'

setup() {
setup_sdcard
cleanup_test_data
mock_system_commands
mkdir -p "$TEST_DATA/TestPackage/App/TestApp"
}

@test "pacman_install.sh copies package files to SDCARD" {
echo '{"label": "New App"}' > "$TEST_DATA/TestPackage/App/TestApp/config.json"

run sh "$PROJECT/src/packageManager/script/pacman_install.sh" "$TEST_DATA" TestPackage

assert_success
assert_file_exists "$SDCARD/App/TestApp/config.json"
}

@test "pacman_install.sh preserves existing label and imgpath" {
# IMPORTANT: Use multi-line JSON - script parser is fragile with single-line
cat > "$TEST_DATA/TestPackage/App/TestApp/config.json" <<EOF
{
"label": "Default",
"imgpath": "/default.png"
}
EOF

mkdir -p "$SDCARD/App/TestApp"
cat > "$SDCARD/App/TestApp/config.json" <<EOF
{
"label": "My Custom Name",
"imgpath": "/my/icon.png"
}
EOF

run sh "$PROJECT/src/packageManager/script/pacman_install.sh" "$TEST_DATA" TestPackage

assert_success
assert_file_contains "$SDCARD/App/TestApp/config.json" '"label": "My Custom Name"'
assert_file_contains "$SDCARD/App/TestApp/config.json" '"imgpath": "/my/icon.png"'
}

@test "pacman_install.sh handles missing existing config gracefully" {
echo '{"label": "Fresh Install"}' > "$TEST_DATA/TestPackage/App/TestApp/config.json"

run sh "$PROJECT/src/packageManager/script/pacman_install.sh" "$TEST_DATA" TestPackage

assert_success
assert_file_contains "$SDCARD/App/TestApp/config.json" '"label": "Fresh Install"'
}
48 changes: 48 additions & 0 deletions test/shell-tests/packages/common/apply.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env bats

load '../../support/test_helper'
load '../../support/mocks'

setup() {
setup_sdcard
cleanup_test_data
mock_system_commands

# Create mock source directory
# apply.sh expects a folder named the same as the target's basename in its own directory
mkdir -p "$TEST_DATA/common/App"
touch "$TEST_DATA/common/App/common_script.sh"
cp "$PROJECT/static/packages/common/apply.sh" "$TEST_DATA/common/apply.sh"

# Create target structure on SDCARD
mkdir -p "$SDCARD/App/Package1"
mkdir -p "$SDCARD/App/Package2"
mkdir -p "$SDCARD/App/NoConfig"

echo '{"name": "p1"}' > "$SDCARD/App/Package1/config.json"
echo '{"name": "p2"}' > "$SDCARD/App/Package2/config.json"
}

@test "apply.sh copies scripts to directories with config.json" {
run sh "$TEST_DATA/common/apply.sh" "$SDCARD/App"

assert_success
assert_file_exists "$SDCARD/App/Package1/common_script.sh"
assert_file_exists "$SDCARD/App/Package2/common_script.sh"
}

@test "apply.sh skips directories without config.json" {
run sh "$TEST_DATA/common/apply.sh" "$SDCARD/App"

assert_success
assert_file_not_exists "$SDCARD/App/NoConfig/common_script.sh"
}

@test "apply.sh exits gracefully if source directory missing" {
rm -rf "$TEST_DATA/common/App"

run sh "$TEST_DATA/common/apply.sh" "$SDCARD/App"

assert_success # Script just exits, no error code
assert_file_not_exists "$SDCARD/App/Package1/common_script.sh"
}
158 changes: 158 additions & 0 deletions test/shell-tests/packages/guest_mode/guest_mode.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#!/usr/bin/env bats

load '../../support/test_helper'
load '../../support/mocks'
load 'guest_mode_helper'

setup() {
cleanup_test_data
mock_system_commands
setup_guest_mode
setup_guest_mode_scripts
mock_themeSwitcher
}

# install.sh tests

@test "Guest Mode: install.sh sets configON when MainProfile exists" {
mkdir -p "$SDCARD/Saves/MainProfile"

cd "$SDCARD/App/Guest_Mode"
run sh ./install.sh

assert_success
run diff ./config.json ./data/configON.json
assert_success
}

@test "Guest Mode: install.sh does nothing when MainProfile missing" {
echo '{"original": true}' > "$SDCARD/App/Guest_Mode/config.json"

cd "$SDCARD/App/Guest_Mode"
run sh ./install.sh

assert_success
assert_file_contains "$SDCARD/App/Guest_Mode/config.json" '"original": true'
}

# saveTime.sh tests

@test "Guest Mode: saveTime.sh saves timestamp to currentTime.txt" {
mock_date "1700000000"

cd "$SDCARD/App/Guest_Mode"
run sh ./saveTime.sh

assert_success
assert_file_exists "$SDCARD/Saves/CurrentProfile/saves/currentTime.txt"
assert_file_contains "$SDCARD/Saves/CurrentProfile/saves/currentTime.txt" "1700000000"
}

# loadTime.sh tests

@test "Guest Mode: loadTime.sh adds default 4-hour offset" {
mock_date "1700000000"
echo "1700000000" > "$SDCARD/Saves/CurrentProfile/saves/currentTime.txt"

cd "$SDCARD/App/Guest_Mode"
run sh ./loadTime.sh

assert_success
assert_file_contains "$MOCK_LOG" "date set to: @1700014400"
}

@test "Guest Mode: loadTime.sh uses custom hours offset" {
mock_date "1700000000"
echo "1700000000" > "$SDCARD/Saves/CurrentProfile/saves/currentTime.txt"
echo "2" > "$SDCARD/.tmp_update/config/startup/addHours"

cd "$SDCARD/App/Guest_Mode"
run sh ./loadTime.sh

assert_success
assert_file_contains "$MOCK_LOG" "date set to: @1700007200"
}

@test "Guest Mode: loadTime.sh skips offset when NTP enabled" {
mock_date "1700000000"
echo "1700000000" > "$SDCARD/Saves/CurrentProfile/saves/currentTime.txt"
touch "$SDCARD/.tmp_update/config/.ntpState"

cd "$SDCARD/App/Guest_Mode"
run sh ./loadTime.sh

assert_success
assert_file_contains "$MOCK_LOG" "date set to: @1700000000"
}

@test "Guest Mode: loadTime.sh handles missing currentTime.txt" {
mock_date "1700000000"
rm -f "$SDCARD/Saves/CurrentProfile/saves/currentTime.txt"

cd "$SDCARD/App/Guest_Mode"
run sh ./loadTime.sh

assert_success
assert_file_contains "$MOCK_LOG" "date set to: @14400"
}

# launch.sh tests

@test "Guest Mode: launch.sh switches Main to Guest" {
mkdir -p "$SDCARD/Saves/GuestProfile/theme"
mkdir -p "$SDCARD/Saves/GuestProfile/lists"
echo "/mnt/SDCARD/Themes/GuestTheme/" > "$SDCARD/Saves/GuestProfile/theme/currentTheme"
echo '{"guest": "favorites"}' > "$SDCARD/Saves/GuestProfile/lists/favorites.json"

echo "/mnt/SDCARD/Themes/MainTheme/" > "$SDCARD/Saves/CurrentProfile/theme/currentTheme"
echo '{"main": "recents"}' > "$SDCARD/Roms/recentlist.json"

mock_date "1700000000"

cd "$SDCARD/App/Guest_Mode"
run sh ./launch.sh

assert_success
assert_dir_exists "$SDCARD/Saves/MainProfile"
assert_dir_not_exists "$SDCARD/Saves/GuestProfile"
}

@test "Guest Mode: launch.sh switches Guest to Main" {
mkdir -p "$SDCARD/Saves/MainProfile/theme"
mkdir -p "$SDCARD/Saves/MainProfile/lists"
echo "/mnt/SDCARD/Themes/MainTheme/" > "$SDCARD/Saves/MainProfile/theme/currentTheme"

mock_date "1700000000"

cd "$SDCARD/App/Guest_Mode"
run sh ./launch.sh

assert_success
assert_dir_exists "$SDCARD/Saves/GuestProfile"
assert_dir_not_exists "$SDCARD/Saves/MainProfile"
}

@test "Guest Mode: launch.sh calls themeSwitcher" {
mkdir -p "$SDCARD/Saves/GuestProfile"
mock_date "1700000000"

cd "$SDCARD/App/Guest_Mode"
run sh ./launch.sh

assert_success
assert_file_contains "$MOCK_LOG" "called themeSwitcher with: --reapply_icons"
}

@test "Guest Mode: launch.sh preserves theme via system.json" {
mkdir -p "$SDCARD/Saves/GuestProfile/theme"
echo "/mnt/SDCARD/Themes/GuestTheme/" > "$SDCARD/Saves/GuestProfile/theme/currentTheme"

mock_date "1700000000"

cd "$SDCARD/App/Guest_Mode"
run sh ./launch.sh

assert_success
run jq -r .theme "$SDCARD/system.json"
assert_output "/mnt/SDCARD/Themes/GuestTheme/"
}
Loading