Drive all levels through UI instrumentation
This commit is contained in:
@@ -36,6 +36,7 @@ ANDROID_NDK_DIR="$SDK_DIR/ndk/27.2.12479018"
|
|||||||
ANDROID_CMAKE_DIR="$SDK_DIR/cmake/3.22.1"
|
ANDROID_CMAKE_DIR="$SDK_DIR/cmake/3.22.1"
|
||||||
ANDROID_TOOLBIN="$ANDROID_NDK_DIR/toolchains/llvm/prebuilt/linux-x86_64/bin"
|
ANDROID_TOOLBIN="$ANDROID_NDK_DIR/toolchains/llvm/prebuilt/linux-x86_64/bin"
|
||||||
STATE_TTL_SECONDS=86400
|
STATE_TTL_SECONDS=86400
|
||||||
|
HIDE_EMULATOR_WINDOW="false"
|
||||||
|
|
||||||
log() {
|
log() {
|
||||||
printf '\n[%s] %s\n' "tooling" "$1"
|
printf '\n[%s] %s\n' "tooling" "$1"
|
||||||
@@ -642,11 +643,16 @@ start_emulator_if_needed() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local emulator_log="$PROJECT_DIR/build/reports/android-emulator.log"
|
local emulator_log="$PROJECT_DIR/build/reports/android-emulator.log"
|
||||||
|
local emulator_window_args=()
|
||||||
|
if [ "$HIDE_EMULATOR_WINDOW" = "true" ]; then
|
||||||
|
emulator_window_args=(-no-window)
|
||||||
|
fi
|
||||||
|
|
||||||
mkdir -p "$(dirname "$emulator_log")"
|
mkdir -p "$(dirname "$emulator_log")"
|
||||||
log "Starting Android emulator: $ANDROID_TEST_AVD_NAME"
|
log "Starting Android emulator: $ANDROID_TEST_AVD_NAME"
|
||||||
"$SDK_DIR/emulator/emulator" \
|
"$SDK_DIR/emulator/emulator" \
|
||||||
-avd "$ANDROID_TEST_AVD_NAME" \
|
-avd "$ANDROID_TEST_AVD_NAME" \
|
||||||
-no-window \
|
"${emulator_window_args[@]}" \
|
||||||
-no-audio \
|
-no-audio \
|
||||||
-no-boot-anim \
|
-no-boot-anim \
|
||||||
-gpu swiftshader_indirect \
|
-gpu swiftshader_indirect \
|
||||||
@@ -788,31 +794,54 @@ maybe_run_operation() {
|
|||||||
|
|
||||||
print_usage() {
|
print_usage() {
|
||||||
cat <<EOF_USAGE
|
cat <<EOF_USAGE
|
||||||
Usage: bash ./AndroidProjectTooling.sh [--build | --build-release-aab | --test | --clean-test | --test-emulator | --compile-git]
|
Usage: bash ./AndroidProjectTooling.sh [--build | --build-release-aab | --test | --clean-test | --test-emulator [--hide-emulator-window] | --compile-git]
|
||||||
|
|
||||||
--build Set up the environment and build the debug APK
|
--build Set up the environment and build the debug APK
|
||||||
--build-release-aab Set up the environment and build a release Android App Bundle (AAB)
|
--build-release-aab Set up the environment and build a release Android App Bundle (AAB)
|
||||||
--test Set up the environment, compile host Git, and run the debug JVM unit tests with it
|
--test Set up the environment, compile host Git, and run the debug JVM unit tests with it
|
||||||
--clean-test Set up the environment, clean Gradle outputs, and run the debug JVM unit tests
|
--clean-test Set up the environment, clean Gradle outputs, and run the debug JVM unit tests
|
||||||
--test-emulator Set up an Android emulator and run debug instrumentation tests on it
|
--test-emulator Set up a visible Android emulator and run debug instrumentation tests on it
|
||||||
|
--hide-emulator-window Run --test-emulator with the emulator window hidden
|
||||||
--compile-git Compile Git for the development host and all Android target ABIs
|
--compile-git Compile Git for the development host and all Android target ABIs
|
||||||
EOF_USAGE
|
EOF_USAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
validate_args() {
|
validate_args() {
|
||||||
case "${1:-}" in
|
local mode="${1:-}"
|
||||||
|
shift || true
|
||||||
|
|
||||||
|
case "$mode" in
|
||||||
""|--build|--build-release-aab|--test|--clean-test|--test-emulator|--compile-git)
|
""|--build|--build-release-aab|--test|--clean-test|--test-emulator|--compile-git)
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown argument: $1" >&2
|
echo "Unknown argument: $mode" >&2
|
||||||
print_usage >&2
|
print_usage >&2
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--hide-emulator-window)
|
||||||
|
if [ "$mode" != "--test-emulator" ]; then
|
||||||
|
echo "--hide-emulator-window can only be used with --test-emulator" >&2
|
||||||
|
print_usage >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
HIDE_EMULATOR_WINDOW="true"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown argument: $1" >&2
|
||||||
|
print_usage >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
validate_args "${1:-}"
|
validate_args "$@"
|
||||||
|
|
||||||
require_tool curl
|
require_tool curl
|
||||||
require_tool unzip
|
require_tool unzip
|
||||||
|
|||||||
20
README.md
20
README.md
@@ -48,7 +48,8 @@ Available commands:
|
|||||||
| `bash ./AndroidProjectTooling.sh` | Provision or refresh the local Android/JDK toolchain only. | Toolchain under `./jdk` and `./android-sdk` |
|
| `bash ./AndroidProjectTooling.sh` | Provision or refresh the local Android/JDK toolchain only. | Toolchain under `./jdk` and `./android-sdk` |
|
||||||
| `bash ./AndroidProjectTooling.sh --test` | Compile host Git, set `GITHUG_TEST_GIT_BINARY`, and run JVM unit tests. | Test reports under `app/build/reports/` |
|
| `bash ./AndroidProjectTooling.sh --test` | Compile host Git, set `GITHUG_TEST_GIT_BINARY`, and run JVM unit tests. | Test reports under `app/build/reports/` |
|
||||||
| `bash ./AndroidProjectTooling.sh --clean-test` | Compile host Git, clean Gradle outputs, set `GITHUG_TEST_GIT_BINARY`, and run JVM unit tests. | Fresh test reports under `app/build/reports/` |
|
| `bash ./AndroidProjectTooling.sh --clean-test` | Compile host Git, clean Gradle outputs, set `GITHUG_TEST_GIT_BINARY`, and run JVM unit tests. | Fresh test reports under `app/build/reports/` |
|
||||||
| `bash ./AndroidProjectTooling.sh --test-emulator` | Install emulator packages if needed, create/start the project test AVD, compile Android Git, and run debug instrumentation tests. | Instrumentation reports under `app/build/reports/androidTests/` |
|
| `bash ./AndroidProjectTooling.sh --test-emulator` | Install emulator packages if needed, create/start the visible project test AVD, compile Android Git, and run debug instrumentation tests. | Instrumentation reports under `app/build/reports/androidTests/` |
|
||||||
|
| `bash ./AndroidProjectTooling.sh --test-emulator --hide-emulator-window` | Run the same emulator instrumentation tests without showing the emulator window. | Instrumentation reports under `app/build/reports/androidTests/` |
|
||||||
| `bash ./AndroidProjectTooling.sh --build` | Build the debug APK. | `app/build/outputs/apk/debug/githug-android-debug-v<versionCode>.apk` |
|
| `bash ./AndroidProjectTooling.sh --build` | Build the debug APK. | `app/build/outputs/apk/debug/githug-android-debug-v<versionCode>.apk` |
|
||||||
| `bash ./AndroidProjectTooling.sh --build-release-aab` | Build the release Android App Bundle. | `app/build/outputs/bundle/release/githug-android-release-v<versionCode>.aab` |
|
| `bash ./AndroidProjectTooling.sh --build-release-aab` | Build the release Android App Bundle. | `app/build/outputs/bundle/release/githug-android-release-v<versionCode>.aab` |
|
||||||
| `bash ./AndroidProjectTooling.sh --compile-git` | Compile Git for the development host and all Android target ABIs. | Host and Android `libgit.so` binaries |
|
| `bash ./AndroidProjectTooling.sh --compile-git` | Compile Git for the development host and all Android target ABIs. | Host and Android `libgit.so` binaries |
|
||||||
@@ -65,6 +66,21 @@ To run instrumentation tests on an Android emulator:
|
|||||||
bash ./AndroidProjectTooling.sh --test-emulator
|
bash ./AndroidProjectTooling.sh --test-emulator
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The emulator window is shown by default so UI instrumentation can be followed visually. Add `--hide-emulator-window` for headless runs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash ./AndroidProjectTooling.sh --test-emulator --hide-emulator-window
|
||||||
|
```
|
||||||
|
|
||||||
|
Test outputs and logs are written under project-local build directories:
|
||||||
|
|
||||||
|
- JVM test HTML report: `app/build/reports/tests/testDebugUnitTest/index.html`
|
||||||
|
- JVM test XML results: `app/build/test-results/testDebugUnitTest/`
|
||||||
|
- Emulator instrumentation HTML report: `app/build/reports/androidTests/connected/debug/index.html`
|
||||||
|
- Emulator instrumentation XML/results: `app/build/outputs/androidTest-results/connected/debug/`
|
||||||
|
- Per-test emulator logcat files: `app/build/outputs/androidTest-results/connected/debug/<device-name>/logcat-*.txt`
|
||||||
|
- Emulator startup log: `build/reports/android-emulator.log`
|
||||||
|
|
||||||
To build installable/debuggable artifacts:
|
To build installable/debuggable artifacts:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -114,7 +130,7 @@ Options:
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `bash ./AndroidProjectTooling.sh --test` | Ensure the host Git binary is current, then run tests with it. | `build/host-git/libgit.so` and test reports |
|
| `bash ./AndroidProjectTooling.sh --test` | Ensure the host Git binary is current, then run tests with it. | `build/host-git/libgit.so` and test reports |
|
||||||
| `bash ./AndroidProjectTooling.sh --clean-test` | Ensure the host Git binary is current, clean Gradle outputs, then run tests with it. | `build/host-git/libgit.so` and fresh test reports |
|
| `bash ./AndroidProjectTooling.sh --clean-test` | Ensure the host Git binary is current, clean Gradle outputs, then run tests with it. | `build/host-git/libgit.so` and fresh test reports |
|
||||||
| `bash ./AndroidProjectTooling.sh --test-emulator` | Ensure Android ABI Git binaries are current, then run instrumentation tests on the project AVD. | Android `libgit.so` binaries and instrumentation reports |
|
| `bash ./AndroidProjectTooling.sh --test-emulator` | Ensure Android ABI Git binaries are current, then run instrumentation tests on the visible project AVD. | Android `libgit.so` binaries and instrumentation reports |
|
||||||
| `bash ./AndroidProjectTooling.sh --compile-git` | Ensure host Git and Android ABI Git binaries are current. | Host and Android outputs |
|
| `bash ./AndroidProjectTooling.sh --compile-git` | Ensure host Git and Android ABI Git binaries are current. | Host and Android outputs |
|
||||||
|
|
||||||
Android ABI outputs are generated files and are ignored by git:
|
Android ABI outputs are generated files and are ignored by git:
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ android {
|
|||||||
applicationId = "solutions.tretter.githugandroid"
|
applicationId = "solutions.tretter.githugandroid"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 173
|
versionCode = 174
|
||||||
versionName = "0.1.172"
|
versionName = "0.1.173"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
@@ -116,8 +116,10 @@ dependencies {
|
|||||||
implementation("com.google.android.material:material:1.12.0")
|
implementation("com.google.android.material:material:1.12.0")
|
||||||
|
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
androidTestImplementation("androidx.test:core:1.6.1")
|
||||||
androidTestImplementation("androidx.test:runner:1.6.1")
|
androidTestImplementation("androidx.test:runner:1.6.1")
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.2.1")
|
androidTestImplementation("androidx.test.ext:junit:1.2.1")
|
||||||
|
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
||||||
|
|
||||||
debugImplementation("androidx.compose.ui:ui-tooling")
|
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||||
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
||||||
|
|||||||
@@ -1,13 +1,27 @@
|
|||||||
package solutions.tretter.githugandroid
|
package solutions.tretter.githugandroid
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
|
import androidx.compose.ui.test.hasText
|
||||||
|
import androidx.compose.ui.test.junit4.createEmptyComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.compose.ui.test.performImeAction
|
||||||
|
import androidx.compose.ui.test.performTextClearance
|
||||||
|
import androidx.compose.ui.test.performTextInput
|
||||||
|
import androidx.test.core.app.ActivityScenario
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import java.io.File
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class GitRepositoryRuntimeInstrumentedTest {
|
class GitRepositoryRuntimeInstrumentedTest {
|
||||||
|
@get:Rule
|
||||||
|
val composeRule = createEmptyComposeRule()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun nativeGitRuntimeCompletesInitLevelOnDevice() {
|
fun nativeGitRuntimeCompletesInitLevelOnDevice() {
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
@@ -24,19 +38,50 @@ class GitRepositoryRuntimeInstrumentedTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun nativeGitRuntimeCompletesConfigLevelOnDevice() {
|
fun allLevelsCompleteThroughAppTerminalUi() {
|
||||||
|
launchFreshApp().use {
|
||||||
|
allGithugLevels().forEachIndexed { index, level ->
|
||||||
|
waitForExercise(level)
|
||||||
|
level.testCases.first().commands.forEach(::submitTerminalCommand)
|
||||||
|
waitForLevelCompletion(nextLevel = allGithugLevels().getOrNull(index + 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchFreshApp(): ActivityScenario<MainActivity> {
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
val runtime = GitRepositoryRuntime(context)
|
File(context.applicationInfo.dataDir, "files").deleteRecursively()
|
||||||
val level = configLevel()
|
File(context.applicationInfo.dataDir, "cache").deleteRecursively()
|
||||||
|
val scenario = ActivityScenario.launch(MainActivity::class.java)
|
||||||
|
waitForExercise(initLevel())
|
||||||
|
return scenario
|
||||||
|
}
|
||||||
|
|
||||||
assertTrue(runtime.unavailableMessage(), runtime.isNativeGitAvailable())
|
private fun waitForExercise(level: Level) {
|
||||||
|
composeRule.waitUntil(timeoutMillis = 30_000) {
|
||||||
|
composeRule.onAllNodes(hasText(level.title, substring = true)).fetchSemanticsNodes().isNotEmpty()
|
||||||
|
}
|
||||||
|
composeRule.onNodeWithTag("exercise-title-${level.id}").assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
val repo = runtime.prepareLevel(level)
|
private fun waitForLevelCompletion(nextLevel: Level?) {
|
||||||
val (namedRepo, _) = runtime.execute(level, repo, "git config user.name GitHug")
|
if (nextLevel == null) {
|
||||||
val (configuredRepo, _) = runtime.execute(level, namedRepo, "git config user.email githug@example.com")
|
composeRule.waitUntil(timeoutMillis = 30_000) {
|
||||||
|
composeRule.onAllNodes(hasText("All Githug levels completed", substring = true)).fetchSemanticsNodes().isNotEmpty()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
composeRule.waitUntil(timeoutMillis = 30_000) {
|
||||||
|
composeRule.onAllNodes(hasText(nextLevel.title, substring = true)).fetchSemanticsNodes().isNotEmpty()
|
||||||
|
}
|
||||||
|
composeRule.onNodeWithTag("exercise-title-${nextLevel.id}").assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
assertTrue(configuredRepo.config["user.name"].orEmpty().isNotBlank())
|
private fun submitTerminalCommand(command: String) {
|
||||||
assertTrue(configuredRepo.config["user.email"].orEmpty().isNotBlank())
|
val input = composeRule.onNodeWithTag("terminal-command-input")
|
||||||
assertTrue(level.validator(configuredRepo, "git config user.email githug@example.com"))
|
input.performClick()
|
||||||
|
input.performTextClearance()
|
||||||
|
input.performTextInput(command)
|
||||||
|
input.performImeAction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,6 @@ struct output_buffer {
|
|||||||
size_t capacity;
|
size_t capacity;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct reader_state {
|
|
||||||
int fd;
|
|
||||||
struct output_buffer output;
|
|
||||||
};
|
|
||||||
|
|
||||||
static pthread_mutex_t git_mutex = PTHREAD_MUTEX_INITIALIZER;
|
static pthread_mutex_t git_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||||
static void *git_handle = NULL;
|
static void *git_handle = NULL;
|
||||||
static git_main_fn git_main = NULL;
|
static git_main_fn git_main = NULL;
|
||||||
@@ -54,23 +49,28 @@ static int append_output(struct output_buffer *buffer, const char *data, size_t
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void *read_output(void *arg) {
|
static int drain_available_output(int fd, struct output_buffer *output, int *saw_eof) {
|
||||||
struct reader_state *state = (struct reader_state *)arg;
|
|
||||||
char chunk[4096];
|
char chunk[4096];
|
||||||
for (;;) {
|
for (;;) {
|
||||||
ssize_t count = read(state->fd, chunk, sizeof(chunk));
|
ssize_t count = read(fd, chunk, sizeof(chunk));
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
if (append_output(&state->output, chunk, (size_t)count) != 0) {
|
if (append_output(output, chunk, (size_t)count) != 0) {
|
||||||
break;
|
return -1;
|
||||||
}
|
}
|
||||||
} else if (count == 0) {
|
continue;
|
||||||
break;
|
|
||||||
} else if (errno != EINTR) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
if (count == 0) {
|
||||||
|
*saw_eof = 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (errno == EINTR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
close(state->fd);
|
|
||||||
return NULL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static jobjectArray make_result(JNIEnv *env, int exit_code, const char *output) {
|
static jobjectArray make_result(JNIEnv *env, int exit_code, const char *output) {
|
||||||
@@ -267,12 +267,36 @@ Java_solutions_tretter_githugandroid_NativeGitBridge_runGitMainNative(
|
|||||||
}
|
}
|
||||||
|
|
||||||
close(pipe_fds[1]);
|
close(pipe_fds[1]);
|
||||||
struct reader_state reader = { .fd = pipe_fds[0], .output = {0} };
|
struct output_buffer output = {0};
|
||||||
read_output(&reader);
|
int flags = fcntl(pipe_fds[0], F_GETFL, 0);
|
||||||
|
if (flags >= 0) {
|
||||||
|
fcntl(pipe_fds[0], F_SETFL, flags | O_NONBLOCK);
|
||||||
|
}
|
||||||
|
|
||||||
int child_status = 0;
|
int child_status = 0;
|
||||||
while (waitpid(child, &child_status, 0) < 0 && errno == EINTR) {
|
int child_done = 0;
|
||||||
|
int saw_eof = 0;
|
||||||
|
while (!child_done || !saw_eof) {
|
||||||
|
if (drain_available_output(pipe_fds[0], &output, &saw_eof) != 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!child_done) {
|
||||||
|
pid_t wait_result = waitpid(child, &child_status, WNOHANG);
|
||||||
|
if (wait_result == child) {
|
||||||
|
child_done = 1;
|
||||||
|
} else if (wait_result < 0 && errno != EINTR) {
|
||||||
|
child_done = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (child_done) {
|
||||||
|
if (!saw_eof) {
|
||||||
|
drain_available_output(pipe_fds[0], &output, &saw_eof);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
usleep(10000);
|
||||||
}
|
}
|
||||||
|
close(pipe_fds[0]);
|
||||||
pthread_mutex_unlock(&git_mutex);
|
pthread_mutex_unlock(&git_mutex);
|
||||||
|
|
||||||
int exit_code = -1;
|
int exit_code = -1;
|
||||||
@@ -282,9 +306,9 @@ Java_solutions_tretter_githugandroid_NativeGitBridge_runGitMainNative(
|
|||||||
exit_code = 128 + WTERMSIG(child_status);
|
exit_code = 128 + WTERMSIG(child_status);
|
||||||
}
|
}
|
||||||
|
|
||||||
jobjectArray result = make_result(env, exit_code, reader.output.data);
|
jobjectArray result = make_result(env, exit_code, output.data);
|
||||||
|
|
||||||
free(reader.output.data);
|
free(output.data);
|
||||||
free_string_array(argv, argc);
|
free_string_array(argv, argc);
|
||||||
free_string_array(env_entries, envc);
|
free_string_array(env_entries, envc);
|
||||||
(*env)->ReleaseStringUTFChars(env, library_path, library_path_chars);
|
(*env)->ReleaseStringUTFChars(env, library_path, library_path_chars);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.graphics.Path
|
import androidx.compose.ui.graphics.Path
|
||||||
import androidx.compose.ui.layout.boundsInRoot
|
import androidx.compose.ui.layout.boundsInRoot
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -51,7 +52,12 @@ fun ExercisePane(
|
|||||||
}
|
}
|
||||||
SelectionContainer {
|
SelectionContainer {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
Text(level.title, style = MaterialTheme.typography.titleLarge, color = TextPrimary)
|
Text(
|
||||||
|
text = level.title,
|
||||||
|
modifier = Modifier.testTag("exercise-title-${level.id}"),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = TextPrimary,
|
||||||
|
)
|
||||||
Text(level.description, color = TextSecondary)
|
Text(level.description, color = TextSecondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -33,7 +34,9 @@ fun LevelsPane(
|
|||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { onSelect(index) },
|
onClick = { onSelect(index) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag("level-${level.id}"),
|
||||||
colors = ButtonDefaults.textButtonColors(
|
colors = ButtonDefaults.textButtonColors(
|
||||||
contentColor = if (index == currentLevelIndex) Accent else TextSecondary,
|
contentColor = if (index == currentLevelIndex) Accent else TextSecondary,
|
||||||
),
|
),
|
||||||
@@ -49,4 +52,4 @@ fun LevelsPane(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import androidx.compose.ui.input.key.key
|
|||||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||||
import androidx.compose.ui.input.key.type
|
import androidx.compose.ui.input.key.type
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.TextRange
|
import androidx.compose.ui.text.TextRange
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
@@ -232,6 +233,7 @@ private fun RowScope.TerminalInputField(
|
|||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
|
.testTag("terminal-command-input")
|
||||||
.focusRequester(focusRequester)
|
.focusRequester(focusRequester)
|
||||||
.onFocusChanged { state ->
|
.onFocusChanged { state ->
|
||||||
if (state.isFocused) {
|
if (state.isFocused) {
|
||||||
|
|||||||
Reference in New Issue
Block a user