Skip to content

test: achieve 100% test coverage for core audio engine #539

test: achieve 100% test coverage for core audio engine

test: achieve 100% test coverage for core audio engine #539

Workflow file for this run

name: CI - Build and Test
on:
push:
branches: [main]
pull_request:
branches: [develop, main]
permissions:
contents: read
pull-requests: write
issues: write
jobs:
test-linux:
name: Test on Linux
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Cache apt packages
uses: actions/cache@v5
id: apt-cache
with:
path: ~/apt-cache
key: apt-${{ runner.os }}-${{ hashFiles('.github/workflows/ci.yml') }}
- name: Install Dependencies
run: |
if [ "${{ steps.apt-cache.outputs.cache-hit }}" = "true" ] && [ -d ~/apt-cache ]; then
echo "Restoring cached .deb packages..."
sudo cp ~/apt-cache/*.deb /var/cache/apt/archives/ 2>/dev/null || true
fi
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends \
build-essential cmake \
libportaudio2 portaudio19-dev \
libsdl2-dev libgl1-mesa-dev libglu1-mesa-dev \
librtmidi-dev
mkdir -p ~/apt-cache
cp /var/cache/apt/archives/*.deb ~/apt-cache/ 2>/dev/null || true
- name: Setup External Dependencies
run: |
bash ./scripts/setup_dependencies.sh
- name: Generate Version
id: version
run: |
COUNT=$(git rev-list --count HEAD)
echo "version=0.1.${COUNT}" >> $GITHUB_OUTPUT
- name: Build
run: |
mkdir -p build && cd build
cmake -DCMAKE_BUILD_TYPE=Debug \
-DAMPLITRON_VERSION="${{ steps.version.outputs.version }}" \
-DCMAKE_C_FLAGS="--coverage -O0 -g" \
-DCMAKE_CXX_FLAGS="--coverage -O0 -g" \
-DCMAKE_EXE_LINKER_FLAGS="--coverage" \
..
make -j$(nproc)
- name: Run Tests
run: cd build && ./amplitron-tests
- name: Package Build Data for Coverage
run: tar -czf build-coverage.tar.gz build/
- name: Upload Build Artifact for Coverage
uses: actions/upload-artifact@v4
with:
name: build-coverage
path: build-coverage.tar.gz
retention-days: 1
- name: Upload Build
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: actions/upload-artifact@v7
with:
name: linux-build
path: build/Amplitron
retention-days: 1
coverage-linux:
name: Coverage Report on Linux
needs: test-linux
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install lcov
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends lcov
- name: Download Build Artifact
uses: actions/download-artifact@v4
with:
name: build-coverage
path: .
- name: Extract Build Data
run: tar -xzf build-coverage.tar.gz
- name: Generate Coverage Report
run: |
lcov --capture --directory build --output-file coverage.info --ignore-errors mismatch,gcov,source
lcov --remove coverage.info \
'/usr/*' \
'*/external/*' \
'*/tests/*' \
'*/build/*' \
--output-file coverage.filtered.info \
--ignore-errors unused
genhtml coverage.filtered.info --output-directory coverage-report
lcov --summary coverage.filtered.info | tee coverage-summary.txt
- name: Enforce Coverage Threshold and Generate Markdown
run: |
COVERAGE=$(lcov --summary coverage.filtered.info 2>&1 | awk '/lines......:/ {print $2}' | sed 's/%//')
echo "Line coverage: $COVERAGE%"
THRESHOLD=60
echo "### Code Coverage Report 📊" > coverage_comment.md
echo "**Line Coverage:** ${COVERAGE}%" >> coverage_comment.md
echo "" >> coverage_comment.md
if awk "BEGIN {exit !($COVERAGE >= $THRESHOLD)}"; then
echo "✅ Coverage meets threshold: $COVERAGE% >= $THRESHOLD%"
echo "✅ Coverage meets threshold: $COVERAGE% >= $THRESHOLD%" >> coverage_comment.md
else
echo "⚠️ Coverage is below threshold: $COVERAGE% < $THRESHOLD%"
echo "⚠️ Coverage is below threshold: $COVERAGE% < $THRESHOLD%" >> coverage_comment.md
echo "This PR adds coverage reporting, so low coverage is reported as a warning."
echo "CI will continue and upload the coverage report."
fi
echo "" >> coverage_comment.md
echo "<details>" >> coverage_comment.md
echo "<summary>Full Coverage Summary</summary>" >> coverage_comment.md
echo "" >> coverage_comment.md
echo '```text' >> coverage_comment.md
cat coverage-summary.txt >> coverage_comment.md
echo '```' >> coverage_comment.md
echo "</details>" >> coverage_comment.md
cat coverage_comment.md >> $GITHUB_STEP_SUMMARY
- name: Save PR Number and Coverage Comment
if: github.event_name == 'pull_request'
run: |
mkdir -p pr_coverage
echo "${{ github.event.pull_request.number }}" > pr_coverage/pr_number.txt
cp coverage_comment.md pr_coverage/coverage_comment.md
- name: Upload PR Coverage Artifact
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@v4
with:
name: pr-coverage-comment
path: pr_coverage/
retention-days: 1
- name: Upload Coverage HTML Report
uses: actions/upload-artifact@v7
with:
name: coverage-report
path: coverage-report/
retention-days: 7
- name: Upload Coverage Summary
uses: actions/upload-artifact@v7
with:
name: coverage-summary
path: |
coverage.filtered.info
coverage-summary.txt
retention-days: 7
test-macos:
name: Test on macOS
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install Dependencies
run: brew install portaudio sdl2 rtmidi
- name: Setup External Dependencies
run: |
bash ./scripts/setup_dependencies.sh
- name: Generate Version
id: version
run: |
COUNT=$(git rev-list --count HEAD)
echo "version=0.1.${COUNT}" >> $GITHUB_OUTPUT
- name: Build
run: |
HOMEBREW_PREFIX=$(brew --prefix)
mkdir -p build && cd build
cmake -DCMAKE_BUILD_TYPE=Release \
-DAMPLITRON_VERSION="${{ steps.version.outputs.version }}" \
-DPORTAUDIO_INCLUDE_DIRS="$HOMEBREW_PREFIX/include" \
-DPORTAUDIO_LIBRARIES="$HOMEBREW_PREFIX/lib/libportaudio.dylib" \
-DSDL2_INCLUDE_DIRS="$HOMEBREW_PREFIX/include/SDL2" \
-DSDL2_LIBRARIES="$HOMEBREW_PREFIX/lib/libSDL2.dylib" \
-DRTMIDI_INCLUDE_DIRS="$HOMEBREW_PREFIX/include" \
-DRTMIDI_LIBRARIES="$HOMEBREW_PREFIX/lib/librtmidi.dylib" \
..
make -j$(sysctl -n hw.ncpu)
- name: Run Tests
run: cd build && ./amplitron-tests
- name: Bundle Dylibs
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: |
set -e
mkdir -p staging
cp build/Amplitron staging/
echo "=== Original linked libraries ==="
otool -L staging/Amplitron
otool -L staging/Amplitron | tail -n +2 | awk '{print $1}' > /tmp/deps.txt
cat /tmp/deps.txt
while IFS= read -r lib; do
case "$lib" in
/usr/lib/*|/System/*|@executable_path/*|@rpath/*|@loader_path/*) continue ;;
esac
libname=$(basename "$lib")
echo ">>> Bundling: $lib -> staging/$libname"
cp -L "$lib" "staging/$libname"
chmod 644 "staging/$libname"
echo ">>> Fixing reference: $lib -> @executable_path/$libname"
install_name_tool -change "$lib" "@executable_path/$libname" staging/Amplitron
done < /tmp/deps.txt
for dylib in staging/*.dylib; do
[ -f "$dylib" ] || continue
libname=$(basename "$dylib")
echo ">>> Fixing dylib id: $libname"
install_name_tool -id "@executable_path/$libname" "$dylib"
otool -L "$dylib" | tail -n +2 | awk '{print $1}' > /tmp/subdeps.txt
while IFS= read -r lib; do
case "$lib" in
/usr/lib/*|/System/*|@executable_path/*|@rpath/*|@loader_path/*) continue ;;
esac
depname=$(basename "$lib")
echo ">>> Fixing sub-dep in $libname: $lib -> @executable_path/$depname"
if [ ! -f "staging/$depname" ]; then
cp -L "$lib" "staging/$depname"
chmod 644 "staging/$depname"
install_name_tool -id "@executable_path/$depname" "staging/$depname"
fi
install_name_tool -change "$lib" "@executable_path/$depname" "$dylib"
done < /tmp/subdeps.txt
done
echo ""
echo "=== After bundling ==="
otool -L staging/Amplitron
echo "=== Dylib links ==="
for f in staging/*.dylib; do
[ -f "$f" ] || continue
echo "--- $(basename $f) ---"
otool -L "$f"
done
echo "=== Staged files ==="
ls -la staging/
- name: Upload Build
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: actions/upload-artifact@v7
with:
name: macos-build
path: staging/
retention-days: 1
build-web:
name: Build Web Demo (Emscripten)
runs-on: ubuntu-latest
env:
EMSDK_VERSION: 3.1.51
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Cache Emscripten SDK & ports
uses: actions/cache@v5
id: emsdk-cache
with:
path: |
emsdk
~/.emscripten_cache
key: emsdk-${{ env.EMSDK_VERSION }}-${{ runner.os }}-${{ hashFiles('CMakeLists.txt') }}
restore-keys: |
emsdk-${{ env.EMSDK_VERSION }}-${{ runner.os }}-
- name: Setup Emscripten
if: steps.emsdk-cache.outputs.cache-hit != 'true'
run: |
if [ -d emsdk ]; then
cd emsdk && git pull
else
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
fi
./emsdk install $EMSDK_VERSION
./emsdk activate $EMSDK_VERSION
- name: Activate Emscripten
run: |
cd emsdk
./emsdk activate $EMSDK_VERSION
echo "${{ github.workspace }}/emsdk" >> $GITHUB_PATH
echo "${{ github.workspace }}/emsdk/upstream/emscripten" >> $GITHUB_PATH
- name: Setup External Dependencies
run: |
bash ./scripts/setup_dependencies.sh
- name: Generate Version
id: version
run: |
COUNT=$(git rev-list --count HEAD)
echo "version=0.1.${COUNT}" >> $GITHUB_OUTPUT
- name: Build WASM
run: |
source emsdk/emsdk_env.sh
mkdir -p build-web && cd build-web
emcmake cmake -DCMAKE_BUILD_TYPE=Release -DAMPLITRON_VERSION="${{ steps.version.outputs.version }}" ..
emmake make -j$(nproc)
- name: Upload Web Build
uses: actions/upload-artifact@v7
with:
name: web-build
path: build-web/
retention-days: 1
test-web:
name: Test Web Demo (Playwright)
needs: build-web
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Download web build artifact
uses: actions/download-artifact@v8
with:
name: web-build
path: web-build/
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "20"
cache: "npm"
cache-dependency-path: tests/web/package.json
- name: Install Playwright & dependencies
working-directory: tests/web
run: |
npm ci
npx playwright install --with-deps chromium
- name: Run Playwright tests
working-directory: tests/web
env:
WEB_BUILD_DIR: ${{ github.workspace }}/web-build
CI: true
run: npx playwright test
- name: Upload Playwright HTML report
if: failure()
uses: actions/upload-artifact@v7
with:
name: playwright-report
path: tests/web/playwright-report/
retention-days: 7
test-windows:
name: Test on Windows
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup MSYS2
uses: msys2/setup-msys2@v2
with:
msystem: MINGW64
update: false
pacboy: >-
gcc:p
cmake:p
ninja:p
ccache:p
portaudio:p
SDL2:p
rtmidi:p
install: git
cache: true
- name: Restore ccache
uses: actions/cache@v5
continue-on-error: true
with:
path: .ccache
key: ccache-windows-${{ runner.os }}-${{ hashFiles('src/**/*.cpp', 'src/**/*.h', 'CMakeLists.txt') }}
restore-keys: |
ccache-windows-${{ runner.os }}-
ccache-windows-
- name: Setup External Dependencies
shell: msys2 {0}
run: |
bash ./scripts/setup_dependencies.sh
- name: Generate Version
shell: msys2 {0}
id: version
run: |
COUNT=$(git rev-list --count HEAD)
echo "version=0.1.${COUNT}" >> $GITHUB_OUTPUT
- name: Build
shell: msys2 {0}
run: |
export CCACHE_DIR="$GITHUB_WORKSPACE/.ccache"
export CCACHE_MAXSIZE=200M
mkdir -p "$CCACHE_DIR"
mkdir -p build && cd build
cmake -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DAMPLITRON_VERSION="${{ steps.version.outputs.version }}" \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
..
ninja -j$NUMBER_OF_PROCESSORS
ccache -s
- name: Run Tests
shell: msys2 {0}
run: cd build && ./amplitron-tests.exe
- name: Collect Binaries
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
shell: msys2 {0}
run: |
mkdir -p staging
cp build/amplitron.exe staging/Amplitron.exe
for dll in libgcc_s_seh-1.dll libstdc++-6.dll libwinpthread-1.dll SDL2.dll; do
cp /mingw64/bin/$dll staging/ 2>/dev/null || true
done
cp /mingw64/bin/libportaudio*.dll staging/ 2>/dev/null || true
cp /mingw64/bin/librtmidi*.dll staging/ 2>/dev/null || true
ls -la staging/
- name: Upload Build
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: actions/upload-artifact@v7
with:
name: windows-build
path: staging/
retention-days: 1
build-android:
name: Build Android APK
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
java-version: "17"
distribution: "temurin"
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v6
- name: Install Android NDK r27
run: |
echo "y" | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager \
"ndk;27.1.12297006" \
"cmake;3.22.1"
- name: Setup External Dependencies
run: |
bash ./scripts/setup_dependencies.sh
- name: Fetch SDL2 Java bridge
run: |
git clone --depth 1 --branch release-2.30.12 \
https://github.com/libsdl-org/SDL.git sdl2-src
mkdir -p android/app/src/main/java/org/libsdl/app
cp sdl2-src/android-project/app/src/main/java/org/libsdl/app/*.java \
android/app/src/main/java/org/libsdl/app/
- name: Generate Version
id: version
run: |
COUNT=$(git rev-list --count HEAD)
echo "version=0.1.${COUNT}" >> $GITHUB_OUTPUT
- name: Build APK
working-directory: android
run: |
./gradlew assembleRelease \
-PamplitronVersion=${{ steps.version.outputs.version }} \
--no-daemon
- name: Validate APK
run: |
APK=$(find android/app/build/outputs/apk/release -name "*.apk" | head -1)
echo "APK: $APK ($(du -sh "$APK" | cut -f1))"
$ANDROID_HOME/build-tools/34.0.0/aapt2 dump badging "$APK" \
| grep -E "^(package|sdkVersion|targetSdkVersion|uses-permission|native-code)"
unzip -p "$APK" lib/arm64-v8a/libmain.so | file - \
| grep -i "ELF.*aarch64" \
|| { echo "ARM64 libmain.so not found or invalid"; exit 1; }
echo "APK validation passed."
- name: Upload APK
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: actions/upload-artifact@v7
with:
name: android-build
path: android/app/build/outputs/apk/release/
retention-days: 1
build-ios:
name: Build iOS App (Device)
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup External Dependencies
run: |
bash ./scripts/setup_dependencies.sh
- name: Generate Version
id: version
run: |
COUNT=$(git rev-list --count HEAD)
echo "version=0.1.${COUNT}" >> $GITHUB_OUTPUT
- name: Cache SDL2 iOS static library
id: sdl2-ios-cache
uses: actions/cache@v5
with:
path: ~/sdl2-ios
key: sdl2-ios-arm64-2.30.12
- name: Build SDL2 for iOS
if: steps.sdl2-ios-cache.outputs.cache-hit != 'true'
run: |
curl -fsSL https://github.com/libsdl-org/SDL/archive/refs/tags/release-2.30.12.tar.gz | tar xz
cmake -S SDL-release-2.30.12 -B sdl2-build \
-G "Unix Makefiles" \
-DCMAKE_SYSTEM_NAME=iOS \
-DCMAKE_OSX_DEPLOYMENT_TARGET=15.0 \
-DCMAKE_OSX_ARCHITECTURES=arm64 \
-DCMAKE_INSTALL_PREFIX=$HOME/sdl2-ios \
-DCMAKE_BUILD_TYPE=Release \
-DSDL_STATIC=ON -DSDL_SHARED=OFF -DSDL_TEST=OFF
cmake --build sdl2-build -j$(sysctl -n hw.ncpu)
cmake --install sdl2-build
- name: Cache CMake configure output
id: cmake-cache
uses: actions/cache@v5
with:
path: build-ios
key: ios-cmake-${{ runner.os }}-${{ hashFiles('CMakeLists.txt', 'ios/Info.plist', '.github/workflows/ci.yml') }}
restore-keys: |
ios-cmake-${{ runner.os }}-
- name: Configure
if: steps.cmake-cache.outputs.cache-hit != 'true'
run: |
cmake \
-DCMAKE_SYSTEM_NAME=iOS \
-DCMAKE_OSX_DEPLOYMENT_TARGET=15.0 \
-DCMAKE_OSX_ARCHITECTURES="arm64" \
-DAMPLITRON_VERSION="${{ steps.version.outputs.version }}" \
-DSDL2_DIR=$HOME/sdl2-ios/lib/cmake/SDL2 \
-G Xcode \
-S . -B build-ios
- name: Query build products path
id: ios-paths
run: |
PRODUCTS_DIR=$(xcodebuild -project build-ios/Amplitron.xcodeproj \
-scheme Amplitron -configuration Release -sdk iphoneos \
-showBuildSettings 2>/dev/null \
| grep ' BUILT_PRODUCTS_DIR' | awk '{print $3}')
echo "products_dir=$PRODUCTS_DIR" >> $GITHUB_OUTPUT
echo "Build products dir: $PRODUCTS_DIR"
- name: Build iOS Device
run: |
set -o pipefail
xcodebuild \
-project build-ios/Amplitron.xcodeproj \
-scheme Amplitron \
-configuration Release \
-sdk iphoneos \
ONLY_ACTIVE_ARCH=YES \
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO \
build 2>&1 | tee /tmp/xcode.log | xcpretty --color; \
STATUS=${PIPESTATUS[0]}; \
[ $STATUS -eq 0 ] || { echo "=== FULL XCODE LOG ==="; cat /tmp/xcode.log; exit $STATUS; }
- name: Validate .app bundle
run: |
PRODUCTS_DIR="${{ steps.ios-paths.outputs.products_dir }}"
APP="$PRODUCTS_DIR/Amplitron.app"
if [ ! -d "$APP" ]; then
echo "Amplitron.app not found at $APP"
echo "Searching workspace..."
find . -name "Amplitron.app" -type d 2>/dev/null || true
exit 1
fi
echo "App bundle: $APP"
BINARY="$APP/Amplitron"
file "$BINARY"
lipo -info "$BINARY" | grep -E "arm64" \
|| { echo "Expected arm64 slice not found"; exit 1; }
plutil -lint "$APP/Info.plist"
BUNDLE_ID=$(/usr/libexec/PlistBuddy -c "Print CFBundleIdentifier" "$APP/Info.plist")
[ "$BUNDLE_ID" = "com.amplitron.app" ] \
|| { echo "Wrong bundle ID: $BUNDLE_ID"; exit 1; }
echo ".app validation passed."
- name: Package .ipa
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: |
APP="${{ steps.ios-paths.outputs.products_dir }}/Amplitron.app"
mkdir -p ios-staging/Payload
cp -R "$APP" ios-staging/Payload/
cd ios-staging && zip -r Amplitron.ipa Payload/
ls -la
- name: Upload iOS App
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: actions/upload-artifact@v7
with:
name: ios-build
path: ios-staging/Amplitron.ipa
retention-days: 1