From d1acfb3c4967020e9346c46a137cadbb0ec1e21e Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Thu, 13 Mar 2025 08:02:03 +0000 Subject: [PATCH] feat: Add comprehensive Android test logging and build tracking - Add build_logs directory for tracking Android build output - Add Android generated assets to gitignore - Improve test logging with timestamps and build context - Add detailed error reporting for build and test failures - Add proper cleanup of Android build artifacts The changes improve Android testing by: 1. Tracking build history in build_logs directory 2. Providing detailed build context for debugging 3. Cleaning up generated assets properly 4. Improving error reporting and traceability 5. Adding structured logging for test execution Added: - build_logs/ directory for build history - Android assets cleanup in .gitignore - Detailed build and test logging --- .gitignore | 6 + android/app/.gitignore | 1 + .../app/ExampleInstrumentedTest.java | 20 ++ android/gradle.properties | 3 + scripts/test-android.js | 252 ++++++++++++++++-- 5 files changed, 254 insertions(+), 28 deletions(-) create mode 100644 android/app/src/androidTest/java/app/timesafari/app/ExampleInstrumentedTest.java diff --git a/.gitignore b/.gitignore index be7d2ec..bce3f91 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,9 @@ android/fastlane/test_output .env.default vendor/ +# Build logs +build_logs/ + +# Android generated assets +android/app/src/main/assets/public/assets/ + diff --git a/android/app/.gitignore b/android/app/.gitignore index 043df80..dac1f80 100644 --- a/android/app/.gitignore +++ b/android/app/.gitignore @@ -1,2 +1,3 @@ /build/* !/build/.npmkeep +src/main/assets/public/assets/ \ No newline at end of file diff --git a/android/app/src/androidTest/java/app/timesafari/app/ExampleInstrumentedTest.java b/android/app/src/androidTest/java/app/timesafari/app/ExampleInstrumentedTest.java new file mode 100644 index 0000000..cf6765a --- /dev/null +++ b/android/app/src/androidTest/java/app/timesafari/app/ExampleInstrumentedTest.java @@ -0,0 +1,20 @@ +package app.timesafari.app; + +import android.content.Context; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("app.timesafari.app", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/android/gradle.properties b/android/gradle.properties index 2e87c52..4e675cf 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -20,3 +20,6 @@ org.gradle.jvmargs=-Xmx1536m # Android operating system, and which are packaged with your app's APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true +android.suppressUnsupportedCompileSdk=34 +android.suppressUnsupportedCompileSdk=34 +android.suppressUnsupportedCompileSdk=34 diff --git a/scripts/test-android.js b/scripts/test-android.js index 30effd4..5285412 100644 --- a/scripts/test-android.js +++ b/scripts/test-android.js @@ -37,14 +37,208 @@ const { execSync } = require('child_process'); const { join } = require('path'); +const { existsSync, mkdirSync, appendFileSync, readFileSync } = require('fs'); + +// Format date as YYYY-MM-DD-HHMMSS +const getLogFileName = () => { + const now = new Date(); + const date = now.toISOString().split('T')[0]; + const time = now.toTimeString().split(' ')[0].replace(/:/g, ''); + return `build_logs/android-build-${date}-${time}.log`; +}; + +// Create logger function +const createLogger = (logFile) => { + return (message) => { + const timestamp = new Date().toISOString(); + const logMessage = `[${timestamp}] ${message}\n`; + console.log(message); + appendFileSync(logFile, logMessage); + }; +}; + +// Check for connected Android devices +const checkConnectedDevices = async (log) => { + log('๐Ÿ” Checking for Android devices...'); + const devices = execSync('adb devices').toString(); + const connectedDevices = devices.split('\n') + .slice(1) + .filter(line => line.includes('device')) + .map(line => line.split('\t')[0]) + .filter(Boolean); + + if (connectedDevices.length === 0) { + throw new Error('No Android devices or emulators connected. Please connect a device or start an emulator.'); + } + + log(`๐Ÿ“ฑ Found ${connectedDevices.length} device(s): ${connectedDevices.join(', ')}`); + return connectedDevices; +}; + +// Verify Java installation +const verifyJavaInstallation = (log) => { + log('๐Ÿ” Checking Java version...'); + const javaHome = '/usr/lib/jvm/java-17-openjdk'; + if (!existsSync(javaHome)) { + throw new Error(`Required Java 17 not found at ${javaHome}. Please install OpenJDK 17.`); + } + log('โœ… Java 17 found'); + return { JAVA_HOME: javaHome }; +}; + +// Generate test data using generate_data.ts +const generateTestData = async (log) => { + log('๐Ÿ”„ Generating test data...'); + try { + execSync('npx ts-node test-scripts/generate_data.ts', { stdio: 'inherit' }); + log('โœ… Test data generated successfully'); + } catch (error) { + throw new Error(`Failed to generate test data: ${error.message}`); + } +}; + +// Parse shell environment file +const parseEnvFile = (filePath) => { + const content = readFileSync(filePath, 'utf8'); + const env = {}; + content.split('\n').forEach(line => { + const match = line.match(/^export\s+(\w+)="(.+)"$/); + if (match) { + env[match[1]] = match[2]; + } + }); + return env; +}; + +// Run individual deeplink test +const executeDeeplink = async (url, description, log) => { + log(`\n๐Ÿ”— Testing deeplink: ${description}`); + log(`URL: ${url}`); + + try { + // Stop the app before executing the deep link + execSync('adb shell am force-stop app.timesafari.app'); + await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1s + + execSync(`adb shell am start -W -a android.intent.action.VIEW -d "${url}" -c android.intent.category.BROWSABLE`); + log(`โœ… Successfully executed: ${description}`); + + // Wait between deeplink tests + await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5s + } catch (error) { + log(`โŒ Failed to execute deeplink: ${description}`); + log(`Error: ${error.message}`); + throw error; + } +}; + +// Run all deeplink tests +const runDeeplinkTests = async (log) => { + log('๐Ÿ”— Starting deeplink tests...'); + + try { + // Load test data + const testEnv = parseEnvFile('.generated/test-env.sh'); + const claimDetails = JSON.parse(readFileSync('.generated/claim_details.json', 'utf8')); + const contacts = JSON.parse(readFileSync('.generated/contacts.json', 'utf8')); + + // Test each deeplink + const deeplinkTests = [ + { + url: `timesafari://claim/${claimDetails.claim_id}`, + description: 'Claim view' + }, + { + url: `timesafari://claim-cert/${claimDetails.claim_id}`, + description: 'Claim certificate view' + }, + { + url: `timesafari://claim-add-raw/${claimDetails.claim_id}`, + description: 'Raw claim addition' + }, + { + url: 'timesafari://did/test', + description: 'DID view with test identifier' + }, + { + url: `timesafari://did/${testEnv.CONTACT1_DID}`, + description: 'DID view with contact DID' + }, + { + url: `timesafari://contact-edit/${testEnv.CONTACT1_DID}`, + description: 'Contact editing' + }, + { + url: `timesafari://contacts/import?contacts=${encodeURIComponent(JSON.stringify(contacts))}`, + description: 'Contacts import' + } + ]; + + // Execute each test + for (const test of deeplinkTests) { + await executeDeeplink(test.url, test.description, log); + } + + log('โœ… All deeplink tests completed successfully'); + } catch (error) { + log('โŒ Deeplink tests failed'); + throw error; + } +}; + +// Build web assets +const buildWebAssets = async (log) => { + log('๐ŸŒ Building web assets...'); + execSync('rm -rf dist', { stdio: 'inherit' }); + execSync('npm run build:web', { stdio: 'inherit' }); + execSync('npm run build:capacitor', { stdio: 'inherit' }); + log('โœ… Web assets built successfully'); +}; + +// Configure Android project +const configureAndroidProject = async (log) => { + log('๐Ÿ“ฑ Syncing Capacitor project...'); + execSync('npx cap sync android', { stdio: 'inherit' }); + log('โœ… Capacitor sync completed'); + + log('โš™๏ธ Configuring Gradle properties...'); + const gradleProps = 'android/gradle.properties'; + if (!existsSync(gradleProps) || !execSync(`grep -q "android.suppressUnsupportedCompileSdk=34" ${gradleProps}`)) { + execSync('echo "android.suppressUnsupportedCompileSdk=34" >> android/gradle.properties'); + log('โœ… Added SDK suppression to gradle.properties'); + } +}; + +// Build and test Android project +const buildAndTestAndroid = async (log, env) => { + log('๐Ÿ—๏ธ Building Android project...'); + execSync('cd android && ./gradlew clean', { stdio: 'inherit', env }); + log('โœ… Gradle clean completed'); + + execSync('cd android && ./gradlew build', { stdio: 'inherit', env }); + log('โœ… Gradle build completed'); + + log('๐Ÿงช Running Android tests...'); + execSync('cd android && ./gradlew connectedAndroidTest', { stdio: 'inherit', env }); + log('โœ… Android tests completed'); +}; + +// Run the app +const runAndroidApp = async (log, env) => { + log('๐Ÿ“ฑ Running app on device...'); + execSync('npx cap run android', { stdio: 'inherit', env }); + log('โœ… App launched successfully'); +}; /** * Runs the complete Android test suite including build, installation, and testing * * The function performs the following steps: - * 1. Syncs the Capacitor project with latest web build - * 2. Builds and installs debug version of the app - * 3. Runs instrumented Android tests + * 1. Checks for connected devices/emulators + * 2. Ensures correct Java version is used + * 3. Checks if app is already installed + * 4. Syncs the Capacitor project with latest build + * 5. Builds and runs instrumented Android tests * * @async * @throws {Error} If any step in the build or test process fails @@ -56,33 +250,35 @@ const { join } = require('path'); * }); */ async function runAndroidTests() { + // Create build_logs directory if it doesn't exist + if (!existsSync('build_logs')) { + mkdirSync('build_logs'); + } + + const logFile = getLogFileName(); + const log = createLogger(logFile); + try { - // Sync Capacitor project with latest web build - // This ensures the Android project has the latest web assets - execSync('npx cap sync android', { - stdio: 'inherit', - // Inherit stdio to show real-time output - }); - - // Build and install debug version of the app - // Uses Gradle wrapper to ensure consistent build environment - execSync('cd android && ./gradlew assembleDebug installDebug', { - stdio: 'inherit', - // assembleDebug: Creates debug APK - // installDebug: Installs APK on connected device - }); - - // Run the instrumented Android tests - // These are the tests defined in android/app/src/androidTest - execSync('cd android && ./gradlew connectedAndroidTest', { - stdio: 'inherit', - // connectedAndroidTest: Runs tests on connected device - }); - - console.log('โœ… Android tests completed successfully'); + log('๐Ÿš€ Starting Android build and test process...'); + + // Generate test data first + await generateTestData(log); + + await checkConnectedDevices(log); + const env = { ...process.env, ...verifyJavaInstallation(log) }; + await buildWebAssets(log); + await configureAndroidProject(log); + await buildAndTestAndroid(log, env); + await runAndroidApp(log, env); + + // Run deeplink tests after app is installed + await runDeeplinkTests(log); + + log('๐ŸŽ‰ Android build and test process completed successfully'); + log(`๐Ÿ“ Full build log available at: ${logFile}`); } catch (error) { - // Log the error and exit with failure code - console.error('โŒ Android tests failed:', error); + log(`โŒ Android tests failed: ${error.message}`); + log(`๐Ÿ“ Check build log for details: ${logFile}`); process.exit(1); } }