Fix config level completion on device
This commit is contained in:
@@ -20,8 +20,8 @@ android {
|
|||||||
applicationId = "solutions.tretter.githugandroid"
|
applicationId = "solutions.tretter.githugandroid"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 174
|
versionCode = 175
|
||||||
versionName = "0.1.173"
|
versionName = "0.1.174"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ 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 java.io.File
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -48,10 +49,27 @@ class GitRepositoryRuntimeInstrumentedTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun configLevelCompletesThroughUiWithArbitraryValues() {
|
||||||
|
launchFreshApp().use {
|
||||||
|
submitTerminalCommand("git init")
|
||||||
|
waitForExercise(configLevel())
|
||||||
|
|
||||||
|
submitTerminalCommand("git config user.name githug")
|
||||||
|
submitTerminalCommand("git config user.email xxx@yy.com")
|
||||||
|
|
||||||
|
waitForLevelCompletion(nextLevel = addLevel())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun launchFreshApp(): ActivityScenario<MainActivity> {
|
private fun launchFreshApp(): ActivityScenario<MainActivity> {
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
File(context.applicationInfo.dataDir, "files").deleteRecursively()
|
File(context.filesDir, "githug-sandboxes").deleteRecursively()
|
||||||
File(context.applicationInfo.dataDir, "cache").deleteRecursively()
|
File(context.applicationInfo.dataDir, "cache").deleteRecursively()
|
||||||
|
runBlocking {
|
||||||
|
GameProgressStore(context.applicationContext).saveProgress(emptySet(), activeLevelId = null)
|
||||||
|
GameProgressStore(context.applicationContext).saveShowHelpOnStart(showHelpOnStart = true)
|
||||||
|
}
|
||||||
val scenario = ActivityScenario.launch(MainActivity::class.java)
|
val scenario = ActivityScenario.launch(MainActivity::class.java)
|
||||||
waitForExercise(initLevel())
|
waitForExercise(initLevel())
|
||||||
return scenario
|
return scenario
|
||||||
|
|||||||
@@ -4,13 +4,16 @@
|
|||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
|
#include <signal.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
|
#include <time.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#define LOG_TAG "GitHugNativeGit"
|
#define LOG_TAG "GitHugNativeGit"
|
||||||
|
#define GIT_COMMAND_TIMEOUT_MS 30000
|
||||||
|
|
||||||
typedef int (*git_main_fn)(int argc, const char **argv);
|
typedef int (*git_main_fn)(int argc, const char **argv);
|
||||||
typedef void (*git_init_fn)(const char **argv);
|
typedef void (*git_init_fn)(const char **argv);
|
||||||
@@ -73,6 +76,14 @@ static int drain_available_output(int fd, struct output_buffer *output, int *saw
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static long long monotonic_millis(void) {
|
||||||
|
struct timespec now;
|
||||||
|
if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return ((long long)now.tv_sec * 1000LL) + ((long long)now.tv_nsec / 1000000LL);
|
||||||
|
}
|
||||||
|
|
||||||
static jobjectArray make_result(JNIEnv *env, int exit_code, const char *output) {
|
static jobjectArray make_result(JNIEnv *env, int exit_code, const char *output) {
|
||||||
jclass string_class = (*env)->FindClass(env, "java/lang/String");
|
jclass string_class = (*env)->FindClass(env, "java/lang/String");
|
||||||
jobjectArray result = (*env)->NewObjectArray(env, 2, string_class, NULL);
|
jobjectArray result = (*env)->NewObjectArray(env, 2, string_class, NULL);
|
||||||
@@ -227,6 +238,17 @@ Java_solutions_tretter_githugandroid_NativeGitBridge_runGitMainNative(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pthread_mutex_lock(&git_mutex);
|
pthread_mutex_lock(&git_mutex);
|
||||||
|
if (load_git(library_path_chars) != 0) {
|
||||||
|
const char *error = dlerror();
|
||||||
|
close(pipe_fds[0]);
|
||||||
|
close(pipe_fds[1]);
|
||||||
|
pthread_mutex_unlock(&git_mutex);
|
||||||
|
free_string_array(argv, argc);
|
||||||
|
free_string_array(env_entries, envc);
|
||||||
|
(*env)->ReleaseStringUTFChars(env, library_path, library_path_chars);
|
||||||
|
(*env)->ReleaseStringUTFChars(env, working_directory, working_directory_chars);
|
||||||
|
return make_error(env, error != NULL ? error : "Native Git execution failed: init_git/cmd_main not found");
|
||||||
|
}
|
||||||
|
|
||||||
pid_t child = fork();
|
pid_t child = fork();
|
||||||
if (child < 0) {
|
if (child < 0) {
|
||||||
@@ -241,6 +263,7 @@ Java_solutions_tretter_githugandroid_NativeGitBridge_runGitMainNative(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (child == 0) {
|
if (child == 0) {
|
||||||
|
setpgid(0, 0);
|
||||||
close(pipe_fds[0]);
|
close(pipe_fds[0]);
|
||||||
dup2(pipe_fds[1], STDOUT_FILENO);
|
dup2(pipe_fds[1], STDOUT_FILENO);
|
||||||
dup2(pipe_fds[1], STDERR_FILENO);
|
dup2(pipe_fds[1], STDERR_FILENO);
|
||||||
@@ -251,20 +274,13 @@ Java_solutions_tretter_githugandroid_NativeGitBridge_runGitMainNative(
|
|||||||
(void)saved_env;
|
(void)saved_env;
|
||||||
chdir(working_directory_chars);
|
chdir(working_directory_chars);
|
||||||
|
|
||||||
if (load_git(library_path_chars) != 0) {
|
|
||||||
const char *error = dlerror();
|
|
||||||
fprintf(stderr, "%s\n", error != NULL ? error : "Native Git execution failed: init_git/cmd_main not found");
|
|
||||||
fflush(stdout);
|
|
||||||
fflush(stderr);
|
|
||||||
_exit(127);
|
|
||||||
}
|
|
||||||
|
|
||||||
git_init((const char **)argv);
|
git_init((const char **)argv);
|
||||||
int child_exit_code = git_main(argc, (const char **)argv);
|
int child_exit_code = git_main(argc, (const char **)argv);
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
fflush(stderr);
|
fflush(stderr);
|
||||||
_exit(child_exit_code);
|
_exit(child_exit_code);
|
||||||
}
|
}
|
||||||
|
setpgid(child, child);
|
||||||
|
|
||||||
close(pipe_fds[1]);
|
close(pipe_fds[1]);
|
||||||
struct output_buffer output = {0};
|
struct output_buffer output = {0};
|
||||||
@@ -276,6 +292,8 @@ Java_solutions_tretter_githugandroid_NativeGitBridge_runGitMainNative(
|
|||||||
int child_status = 0;
|
int child_status = 0;
|
||||||
int child_done = 0;
|
int child_done = 0;
|
||||||
int saw_eof = 0;
|
int saw_eof = 0;
|
||||||
|
int timed_out = 0;
|
||||||
|
long long started_at = monotonic_millis();
|
||||||
while (!child_done || !saw_eof) {
|
while (!child_done || !saw_eof) {
|
||||||
if (drain_available_output(pipe_fds[0], &output, &saw_eof) != 0) {
|
if (drain_available_output(pipe_fds[0], &output, &saw_eof) != 0) {
|
||||||
break;
|
break;
|
||||||
@@ -288,6 +306,16 @@ Java_solutions_tretter_githugandroid_NativeGitBridge_runGitMainNative(
|
|||||||
child_done = 1;
|
child_done = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!child_done && started_at > 0 && monotonic_millis() - started_at > GIT_COMMAND_TIMEOUT_MS) {
|
||||||
|
timed_out = 1;
|
||||||
|
kill(-child, SIGKILL);
|
||||||
|
kill(child, SIGKILL);
|
||||||
|
while (waitpid(child, &child_status, 0) < 0 && errno == EINTR) {
|
||||||
|
}
|
||||||
|
child_done = 1;
|
||||||
|
const char *timeout_message = "\nNative Git execution timed out.\n";
|
||||||
|
append_output(&output, timeout_message, strlen(timeout_message));
|
||||||
|
}
|
||||||
if (child_done) {
|
if (child_done) {
|
||||||
if (!saw_eof) {
|
if (!saw_eof) {
|
||||||
drain_available_output(pipe_fds[0], &output, &saw_eof);
|
drain_available_output(pipe_fds[0], &output, &saw_eof);
|
||||||
@@ -300,7 +328,9 @@ Java_solutions_tretter_githugandroid_NativeGitBridge_runGitMainNative(
|
|||||||
pthread_mutex_unlock(&git_mutex);
|
pthread_mutex_unlock(&git_mutex);
|
||||||
|
|
||||||
int exit_code = -1;
|
int exit_code = -1;
|
||||||
if (WIFEXITED(child_status)) {
|
if (timed_out) {
|
||||||
|
exit_code = 124;
|
||||||
|
} else if (WIFEXITED(child_status)) {
|
||||||
exit_code = WEXITSTATUS(child_status);
|
exit_code = WEXITSTATUS(child_status);
|
||||||
} else if (WIFSIGNALED(child_status)) {
|
} else if (WIFSIGNALED(child_status)) {
|
||||||
exit_code = 128 + WTERMSIG(child_status);
|
exit_code = 128 + WTERMSIG(child_status);
|
||||||
|
|||||||
@@ -200,9 +200,33 @@ internal class GitRepositoryInspector(
|
|||||||
userEmailResult.outputLines.firstOrNull()
|
userEmailResult.outputLines.firstOrNull()
|
||||||
?.takeIf { userEmailResult.exitCode == 0 && it.isNotBlank() }
|
?.takeIf { userEmailResult.exitCode == 0 && it.isNotBlank() }
|
||||||
?.let { put("user.email", it) }
|
?.let { put("user.email", it) }
|
||||||
|
putAll(readGitConfigFile(inspectionRoot).filterKeys { it !in keys })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun readGitConfigFile(inspectionRoot: File): Map<String, String> {
|
||||||
|
val configFile = File(inspectionRoot, ".git/config")
|
||||||
|
if (!configFile.isFile) return emptyMap()
|
||||||
|
|
||||||
|
val entries = linkedMapOf<String, String>()
|
||||||
|
var section = ""
|
||||||
|
configFile.forEachLine { rawLine ->
|
||||||
|
val line = rawLine.trim()
|
||||||
|
when {
|
||||||
|
line.isBlank() || line.startsWith("#") || line.startsWith(";") -> return@forEachLine
|
||||||
|
line.startsWith("[") && line.endsWith("]") -> {
|
||||||
|
section = line.removePrefix("[").removeSuffix("]").trim().substringBefore(' ')
|
||||||
|
}
|
||||||
|
"=" in line && section.isNotBlank() -> {
|
||||||
|
val key = line.substringBefore('=').trim()
|
||||||
|
val value = line.substringAfter('=').trim()
|
||||||
|
entries["$section.$key"] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
private fun inspectRemoteRefs(git: File, inspectionRoot: File, remoteLines: List<String>): RemoteRefs {
|
private fun inspectRemoteRefs(git: File, inspectionRoot: File, remoteLines: List<String>): RemoteRefs {
|
||||||
val remotes = remoteLines.mapNotNull { line ->
|
val remotes = remoteLines.mapNotNull { line ->
|
||||||
val parts = line.trim().split(Regex("\\s+"))
|
val parts = line.trim().split(Regex("\\s+"))
|
||||||
|
|||||||
@@ -268,12 +268,12 @@ class GitSandboxEngineTest {
|
|||||||
val level = configLevel()
|
val level = configLevel()
|
||||||
val repo = runtime.prepareLevel(level)
|
val repo = runtime.prepareLevel(level)
|
||||||
|
|
||||||
val (namedRepo, _) = runtime.execute(level, repo, "git config user.name GitHug")
|
val (namedRepo, _) = runtime.execute(level, repo, "git config user.name githug")
|
||||||
val (configuredRepo, _) = runtime.execute(level, namedRepo, "git config user.email githug@example.com")
|
val (configuredRepo, _) = runtime.execute(level, namedRepo, "git config user.email xxx@yy.com")
|
||||||
|
|
||||||
assertEquals("GitHug", configuredRepo.config["user.name"])
|
assertEquals("githug", configuredRepo.config["user.name"])
|
||||||
assertEquals("githug@example.com", configuredRepo.config["user.email"])
|
assertEquals("xxx@yy.com", configuredRepo.config["user.email"])
|
||||||
assertTrue(level.validator(configuredRepo, "git config user.email githug@example.com"))
|
assertTrue(level.validator(configuredRepo, "git config user.email xxx@yy.com"))
|
||||||
} finally {
|
} finally {
|
||||||
root.deleteRecursively()
|
root.deleteRecursively()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user