Skip to content

Commit 2d692e8

Browse files
authored
Enable automatic Android tests (#198)
This PR cleans up the `log4j-samples-android` project and enables a CI workflow to check the compatibility of releases with Android.
1 parent 9dd6450 commit 2d692e8

File tree

21 files changed

+560
-305
lines changed

21 files changed

+560
-305
lines changed

.github/workflows/android-reusable-test.yaml

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -41,41 +41,68 @@ jobs:
4141
- name: Setup Java
4242
uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # 4.4.0
4343
with:
44+
distribution: 'temurin'
4445
java-version: 17
4546

46-
- name: Setup Android SDK
47+
- name: Setup Gradle
48+
uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # 4.1.0
49+
50+
- name: Enable KVM
51+
run: |
52+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
53+
sudo udevadm control --reload-rules
54+
sudo udevadm trigger --name-match=kvm
55+
56+
- name: Create AVD Device
57+
id: avd
4758
shell: bash
4859
env:
4960
ANDROID_SDK_VERSION: 10406996
5061
ANDROID_SDK_CHECKSUM: 8919e8752979db73d8321e9babe2caedcc393750817c1a5f56c128ec442fb540
5162
run: |
52-
# Create home directory
53-
export ANDROID_HOME=$HOME/.android/sdk
54-
mkdir -p $ANDROID_HOME
55-
56-
# Download and verify the `cmdline-tools`
57-
curl -O $ANDROID_HOME/cmdline-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_SDK_VERSION}_latest.zip
58-
echo "$ANDROID_SDK_CHECKSUM $ANDROID_HOME/cmdline-tools.zip" | sha256sum -c
59-
unzip -d $ANDROID_HOME/cmdline-tools{,.zip}
60-
mv $ANDROID_HOME/cmdline-tools/{cmdline-tools,latest}
61-
62-
# Accept licenses
63-
yes | $ANDROID_HOME/cmdline-tools/bin/sdkmanager --licenses
64-
65-
# Installing the remaining tools
66-
$ANDROID_HOME/cmdline-tools/bin/sdkmanager tools platform-tools
67-
68-
# Exporting ANDROID_HOME and adding sdkmanager to the PATH
69-
echo "ANDROID_HOME=$ANDROID_HOME" >> $GITHUB_ENV
70-
echo $ANDROID_HOME/cmdline-tools/bin >> $GITHUB_PATH
63+
# List installed and available packages
64+
$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --list
65+
# Download images
66+
echo y | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager \
67+
--install "system-images;android-31;default;x86_64" \
68+
emulator platform-tools
69+
# Create device
70+
$ANDROID_HOME/cmdline-tools/latest/bin/avdmanager create avd \
71+
--name generic-api-31-device \
72+
--device "5.4in FWVGA" \
73+
--package "system-images;android-31;default;x86_64"
74+
# Run emulator
75+
$ANDROID_HOME/emulator/emulator \
76+
-no-audio -no-window \
77+
-avd generic-api-31-device &
78+
# Enabled the cleanup job
79+
echo EMULATOR_STARTED=true >> $GITHUB_ENV
80+
# Wait for device to go online
81+
# It might take up to 5 minutes
82+
for i in {1..300}; do
83+
# Don't stop the script if `adb` fails
84+
boot_completed=$($ANDROID_HOME/platform-tools/adb shell getprop sys.boot_completed 2> /dev/null || echo "0")
85+
if [ "${boot_completed}" = "1" ]; then break; fi
86+
sleep 1
87+
done
7188
7289
- name: Build
7390
id: build
7491
shell: bash
7592
env:
7693
LOG4J_VERSION: ${{ inputs.log4j-version }}
7794
run: |
78-
log4j-samples-android/gradlew -p log4j-samples-gradlew \
95+
log4j-samples-android/gradlew -p log4j-samples-android \
7996
--console plain \
80-
-Plog4jVersion=$LOG4J_VERSION \
97+
-Plog4j.version=$LOG4J_VERSION \
8198
build connectedCheck
99+
100+
- name: Remove AVD Device
101+
if: ${{ always() && env.EMULATOR_STARTED == 'true' }}
102+
shell: bash
103+
run: |
104+
# Kill the emulator
105+
$ANDROID_HOME/platform-tools/adb -s emulator-5554 emu kill
106+
# Delete the device
107+
$ANDROID_HOME/cmdline-tools/latest/bin/avdmanager delete avd \
108+
--name generic-api-31-device

.github/workflows/android-test.yaml

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,16 @@ name: android-test
1919

2020
on:
2121
workflow_dispatch:
22-
schedule:
23-
- cron: "29 17 * * *"
22+
inputs:
23+
log4j-version:
24+
description: The version of Log4j Core to use
25+
default: "2.25.0-SNAPSHOT"
2426

2527
permissions: read-all
2628

2729
jobs:
2830

2931
android-test:
30-
strategy:
31-
fail-fast: false
32-
matrix:
33-
log4j-version: [ "2.25.0-SNAPSHOT" ]
34-
uses: apache/logging-log4j-samples/.github/workflows/android-reusable-test.yaml@main
32+
uses: apache/logging-log4j-samples/.github/workflows/android-reusable-test.yaml@feature/android-tests
3533
with:
36-
log4j-version: ${{ matrix.log4j-version }}
34+
log4j-version: ${{ inputs.log4j-version }}

README.adoc

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,26 @@ limitations under the License.
1818
1919
This repository contains several examples to help the user with some of the more advanced Log4j features.
2020
21+
xref:log4j-samples-android/README.adoc[`log4j-samples-android`]::
22+
Explains how to use Log4j Core on Android.
23+
2124
xref:log4j-samples-configuration/README.adoc[`log4j-samples-configuration`]::
22-
Explains how to use a custom programmatic configuration,
25+
Explains how to use a custom programmatic configuration.
2326
2427
xref:log4j-samples-flume-embedded/README.adoc[`log4j-samples-flume-embedded`]::
25-
Explains how to use an embedded Flume agent to send logs to https://flume.apache.org[Apache Flume],
28+
Explains how to use an embedded Flume agent to send logs to https://flume.apache.org[Apache Flume.]
2629
2730
xref:log4j-samples-flume-remote/README.adoc[`log4j-samples-flume-remote`]::
2831
Explains how to send logs to https://flume.apache.org[Apache Flume],
2932
33+
xref:log4j-samples-graalvm/README.adoc[`log4j-samples-graalvm`]::
34+
Explains how to use Log4j API and its implementation to create native GraalVM images.
35+
3036
xref:log4j-samples-jlink/README.adoc[`log4j-samples-jlink`]::
3137
An example of JLink custom JRE.
3238
3339
xref:log4j-samples-loggerProperties/README.adoc[`log4j-samples-loggerProperties`]::
34-
Explains how to write a custom property lookup,
40+
Explains how to write a custom property lookup.
3541
3642
xref:log4j-spring-cloud-config-sample-application/README.md[`log4j-spring-cloud-config-sample-application`]::
3743
An example of Spring Boot application that reads its logging configuration from a Spring Cloud Configuration Server.

log4j-samples-android/README.adoc

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
////
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
https://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
////
17+
= Apache Log4j Samples: Android application
18+
19+
This project is an example of Android application that uses
20+
https://logging.apache.org/log4j/2.x/manual/api.html[the Log4j API]
21+
for logging together with its reference implementation
22+
https://logging.apache.org/log4j/2.x/manual/implementation.html[Log4j Core].
23+
24+
To build the project, run:
25+
26+
[source,shell]
27+
----
28+
./gradlew build
29+
----
30+
31+
inside this directory.
32+
33+
Since version `2.25.0` this application is part of the integration tests for a Log4j release.
34+
35+
== Known Android limitations
36+
37+
The Android platform does not implement all the features of a standard JRE, which will prevent you from using all the features offered by Log4j Core.
38+
For a list of known limitations, see
39+
https://logging.apache.org/log4j/2.x/faq.html#android[our Android F.A.Q. entry].
40+

log4j-samples-android/app/build.gradle

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ plugins {
2020

2121
android {
2222
compileSdk 34
23+
namespace "org.apache.logging.log4j.samples.android"
2324

2425
defaultConfig {
2526
applicationId "org.apache.logging.log4j.samples.android"
@@ -48,17 +49,29 @@ android {
4849
}
4950
}
5051

52+
def log4jVersion = providers.gradleProperty("log4j.version").get()
53+
5154
dependencies {
5255

53-
implementation 'androidx.appcompat:appcompat:1.6.1'
54-
implementation 'com.google.android.material:material:1.11.0'
56+
implementation 'androidx.appcompat:appcompat:1.7.0'
57+
implementation 'com.google.android.material:material:1.12.0'
5558
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
5659

5760
testImplementation 'junit:junit:4.13.2'
5861
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
5962
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
63+
androidTestImplementation 'org.assertj:assertj-core:3.26.3'
64+
65+
// Log4j
66+
implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: log4jVersion
67+
implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: log4jVersion
68+
androidTestImplementation(group: 'org.apache.logging.log4j', name: 'log4j-core-test', version: log4jVersion) {
69+
exclude group: 'com.google.code.java-allocation-instrumenter'
70+
exclude group: 'org.apache.logging.log4j', module: 'log4j-api-test'
71+
exclude group: 'org.hamcrest'
72+
exclude group: 'org.junit.jupiter'
73+
exclude group: 'org.junit.platform'
74+
exclude group: 'org.springframework'
75+
}
6076

61-
//Log4j
62-
implementation 'org.apache.logging.log4j:log4j-api:2.24.1'
63-
implementation 'org.apache.logging.log4j:log4j-core:2.24.1'
64-
}
77+
}

log4j-samples-android/app/src/androidTest/java/org/apache/logging/log4j/samples/android/ExampleInstrumentedTest.java

Lines changed: 0 additions & 42 deletions
This file was deleted.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.logging.log4j.samples.android;
18+
19+
import static androidx.test.espresso.Espresso.onView;
20+
import static androidx.test.espresso.action.ViewActions.click;
21+
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
22+
import static androidx.test.espresso.assertion.ViewAssertions.matches;
23+
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
24+
import static androidx.test.espresso.matcher.ViewMatchers.withId;
25+
import static androidx.test.espresso.matcher.ViewMatchers.withText;
26+
import static org.assertj.core.api.Assertions.assertThat;
27+
28+
import androidx.test.ext.junit.rules.ActivityScenarioRule;
29+
import androidx.test.ext.junit.runners.AndroidJUnit4;
30+
import java.util.AbstractMap;
31+
import java.util.Arrays;
32+
import java.util.Map;
33+
import org.apache.logging.log4j.Level;
34+
import org.apache.logging.log4j.Logger;
35+
import org.apache.logging.log4j.core.Appender;
36+
import org.apache.logging.log4j.core.LoggerContext;
37+
import org.apache.logging.log4j.core.appender.ConsoleAppender;
38+
import org.apache.logging.log4j.core.config.Configuration;
39+
import org.apache.logging.log4j.core.config.LoggerConfig;
40+
import org.apache.logging.log4j.core.test.appender.ListAppender;
41+
import org.junit.BeforeClass;
42+
import org.junit.Rule;
43+
import org.junit.Test;
44+
import org.junit.runner.RunWith;
45+
46+
/**
47+
* Instrumented test, which will execute on an Android device.
48+
*/
49+
@RunWith(AndroidJUnit4.class)
50+
public class MainActivityTest {
51+
52+
@Rule
53+
public ActivityScenarioRule<MainActivity> activityScenarioRule = new ActivityScenarioRule<>(MainActivity.class);
54+
55+
private static Configuration configuration;
56+
private static ListAppender appender;
57+
private static Logger logger;
58+
59+
@BeforeClass
60+
public static void setup() {
61+
LoggerContext context = LoggerContext.getContext(false);
62+
configuration = context.getConfiguration();
63+
appender = configuration.getAppender("LIST");
64+
logger = context.getLogger(MainActivity.class);
65+
}
66+
67+
@Test
68+
public void hasExpectedConfiguration() {
69+
assertThat(configuration.getConfigurationSource().getLocation())
70+
.startsWith("jar:file:")
71+
.endsWith("!/log4j2-test.xml");
72+
// Check appenders
73+
Appender console = configuration.getAppender("CONSOLE");
74+
assertThat(console).as("Console Appender").isInstanceOf(ConsoleAppender.class);
75+
Appender list = configuration.getAppender("LIST");
76+
assertThat(list).as("List Appender").isInstanceOf(ListAppender.class);
77+
// Check logger configurations
78+
assertThat(configuration.getLoggers()).hasSize(2);
79+
assertThat(configuration.getRootLogger().getExplicitLevel()).isEqualTo(Level.INFO);
80+
assertThat(configuration.getRootLogger().getAppenders())
81+
.hasSize(2)
82+
.containsExactly(entry("CONSOLE", console), entry("LIST", list));
83+
assertThat(configuration.getLoggerConfig(MainActivity.class.getName()))
84+
.isNotNull()
85+
.extracting(LoggerConfig::getExplicitLevel)
86+
.isEqualTo(Level.DEBUG);
87+
}
88+
89+
@Test
90+
public void logMessagesAreDelivered() {
91+
assertThat(logger.getLevel()).isEqualTo(Level.DEBUG);
92+
appender.clear();
93+
for (int buttonId : MainActivity.buttonIds) {
94+
onView(withId(buttonId)).perform(click());
95+
}
96+
Level[] expectedLevels = {Level.FATAL, Level.ERROR, Level.WARN, Level.INFO, Level.DEBUG};
97+
String[] expectedMessages =
98+
Arrays.stream(expectedLevels).map(l -> l + "-Hello " + l + "!").toArray(String[]::new);
99+
assertThat(appender.getMessages()).hasSize(expectedLevels.length).containsExactly(expectedMessages);
100+
}
101+
102+
@Test
103+
public void logLevelChanges() {
104+
assertThat(logger.getLevel()).isEqualTo(Level.DEBUG);
105+
onView(withId(R.id.setLogLevelBtn)).perform(click());
106+
onView(withText("Set log level")).check(matches(isDisplayed()));
107+
onView(withText("ERROR")).perform(click());
108+
onView(withText("Select")).perform(click());
109+
onView(withText("Set log level")).check(doesNotExist());
110+
assertThat(logger.getLevel()).isEqualTo(Level.ERROR);
111+
}
112+
113+
private static Map.Entry<String, Appender> entry(String name, Appender appender) {
114+
return new AbstractMap.SimpleImmutableEntry<>(name, appender);
115+
}
116+
}

0 commit comments

Comments
 (0)