Integrate Android into /t:Test (#35640)
authorEgor Bogatov <egorbo@gmail.com>
Thu, 30 Apr 2020 09:44:50 +0000 (12:44 +0300)
committerGitHub <noreply@github.com>
Thu, 30 Apr 2020 09:44:50 +0000 (11:44 +0200)
Integrate Android into the "Test" workflow, e.g.:
```
./build.sh -os Android -arch arm64 -subset Mono+Libs

cd src/libraries/System.Buffers/tests

dotnet build /p:TargetOS=Android /p:TargetArchitecture=arm64 /t:Test
```
^ should run tests for any test suite on an active device or emulator.

eng/testing/AndroidRunnerTemplate.sh [new file with mode: 0644]
eng/testing/AppleRunnerTemplate.sh [moved from eng/testing/MobileRunnerTemplate.sh with 100% similarity]
eng/testing/tests.targets
src/libraries/System.Console/src/System/ConsolePal.Android.cs
src/mono/msbuild/AndroidAppBuilder/Templates/AndroidManifest.xml
src/mono/msbuild/AndroidAppBuilder/Templates/MonoRunner.java
src/mono/msbuild/AndroidAppBuilder/Templates/runtime-android.c
src/mono/msbuild/AndroidTestRunner/AndroidTestRunner.cs

diff --git a/eng/testing/AndroidRunnerTemplate.sh b/eng/testing/AndroidRunnerTemplate.sh
new file mode 100644 (file)
index 0000000..ceb5d53
--- /dev/null
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+
+EXECUTION_DIR=$(dirname $0)
+TEST_NAME=$1
+TARGET_ARCH=$2
+
+APK=$EXECUTION_DIR/Bundle/bin/$TEST_NAME.apk
+
+# it doesn't support parallel execution yet, so, here is a hand-made semaphore:
+LOCKDIR=/tmp/androidtests.lock
+while true; do
+    if mkdir "$LOCKDIR"
+    then
+        trap 'rm -rf "$LOCKDIR"' 0
+        break
+    else
+        sleep 5
+    fi
+done
+
+
+## XHarness doesn't support macOS/Linux yet (in progress) so we'll use a hand-made adb script
+# dotnet xharness android test -i="net.dot.MonoRunner" \
+# --package-name="net.dot.$TEST_NAME" \
+# --app=$APK -o=$EXECUTION_DIR/Bundle/TestResults -v
+
+ADB=$ANDROID_SDK_ROOT/platform-tools/adb
+echo "Installing net.dot.$TEST_NAME on an active device/emulator..."
+$ADB uninstall net.dot.$TEST_NAME > /dev/null 2>&1 || true
+$ADB install "$APK"
+echo "Running tests for $TEST_NAME (see live logs via logcat)..."
+$ADB shell am instrument -w net.dot.$TEST_NAME/net.dot.MonoRunner
+echo "Finished. See logcat for details, e.g. '$ADB logcat DOTNET:V -s'"
index a2cfbd0..bccb7df 100644 (file)
@@ -4,7 +4,8 @@
 
     <RunScriptInputName Condition="'$(TargetOS)' == 'Windows_NT'">RunnerTemplate.cmd</RunScriptInputName>
     <RunScriptInputName Condition="'$(TargetOS)' != 'Windows_NT'">RunnerTemplate.sh</RunScriptInputName>
-    <RunScriptInputName Condition="'$(TargetOS)' == 'iOS'">MobileRunnerTemplate.sh</RunScriptInputName>
+    <RunScriptInputName Condition="'$(TargetOS)' == 'iOS'">AppleRunnerTemplate.sh</RunScriptInputName>
+    <RunScriptInputName Condition="'$(TargetOS)' == 'Android'">AndroidRunnerTemplate.sh</RunScriptInputName>
 
     <RunScriptInputPath>$(MSBuildThisFileDirectory)$(RunScriptInputName)</RunScriptInputPath>
 
@@ -98,7 +99,7 @@
     <PropertyGroup>
       <RunTestsCommand>"$(RunScriptOutputPath)" --runtime-path "$(TestHostRootPath.TrimEnd('\/'))"</RunTestsCommand>
       <RunTestsCommand Condition="'$(TestRspFile)' != '' and '$(RuntimeFlavor)' != 'Mono'">$(RunTestsCommand) --rsp-file "$(TestRspFile)"</RunTestsCommand>
-      <RunTestsCommand Condition="'$(TargetOS)'=='iOS'">"$(RunScriptOutputPath)" $(AssemblyName) $(TargetArchitecture)</RunTestsCommand>
+      <RunTestsCommand Condition="'$(TargetsMobile)'">"$(RunScriptOutputPath)" $(AssemblyName) $(TargetArchitecture)</RunTestsCommand>
     </PropertyGroup>
 
     <!-- Invoke the run script with the test host as the runtime path. -->
         <Output TaskParameter="ApkPackageId"  PropertyName="ApkPackageId" />
         <Output TaskParameter="ApkBundlePath" PropertyName="ApkBundlePath" />
     </AndroidAppBuilderTask>
-    <Message Importance="High" Text="PackageId: $(ApkPackageId)"/>
-    <Message Importance="High" Text="Apk:       $(ApkBundlePath)"/>
+    <Message Importance="High" Text="PackageId:       $(ApkPackageId)"/>
+    <Message Importance="High" Text="Instrumentation: net.dot.MonoRunner"/>
+    <Message Importance="High" Text="Apk:             $(ApkBundlePath)"/>
   </Target>
 
   <!-- Generate a self-contained app bundle for iOS with tests.
index d9aa164..87958be 100644 (file)
@@ -17,7 +17,7 @@ namespace System
         public override unsafe void Write(byte[] buffer, int offset, int count)
         {
             string log = ConsolePal.OutputEncoding.GetString(buffer, offset, count);
-            Interop.Logcat.AndroidLogPrint(Interop.Logcat.LogLevel.Info, null, log);
+            Interop.Logcat.AndroidLogPrint(Interop.Logcat.LogLevel.Info, "DOTNET", log);
         }
     }
 
index 21dc552..12371db 100644 (file)
@@ -5,6 +5,8 @@
           a:versionName="1.0">
   <uses-sdk a:minSdkVersion="%MinSdkLevel%" />
   <uses-permission a:name="android.permission.INTERNET"/>
+  <uses-permission a:name="android.permission.READ_EXTERNAL_STORAGE"/>
+  <uses-permission a:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
   <application a:label="%PackageName%" 
                a:largeHeap="true">
     <activity a:name="net.dot.MainActivity">
index 87f75b5..2323f83 100644 (file)
@@ -14,6 +14,7 @@ import android.util.Log;
 import android.view.View;
 import android.app.Activity;
 import android.os.Bundle;
+import android.os.Environment;
 
 import java.io.File;
 import java.io.FileNotFoundException;
@@ -44,25 +45,22 @@ public class MonoRunner extends Instrumentation
         Context context = getContext();
         AssetManager am = context.getAssets();
         String filesDir = context.getFilesDir().getAbsolutePath();
-        String cacheDir = context.getCacheDir().getAbsolutePath ();
+        String cacheDir = context.getCacheDir().getAbsolutePath();
+        String docsDir  = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS).getAbsolutePath();
 
         copyAssetDir(am, "", filesDir);
 
-        // retcode is what Main() returns in C#
-        int retcode = initRuntime(filesDir, cacheDir);
-        WriteLineToInstrumentation("[Mono] Main() returned " + retcode);
-        runOnMainSync (new Runnable() {
+        int retcode = initRuntime(filesDir, cacheDir, docsDir);
+        runOnMainSync(new Runnable() {
             public void run() {
-                finish (retcode, null);
+                Bundle result = new Bundle();
+                result.putInt("return-code", retcode);
+                // Xharness cli expects "test-results-path" with test results
+                result.putString("test-results-path", docsDir + "/testResults.xml");
+                finish(retcode, result);
             }
         });
     }
-    
-    static void WriteLineToInstrumentation(String line) {
-        Bundle b = new Bundle();
-        b.putString(Instrumentation.REPORT_KEY_STREAMRESULT, line + "\n");
-        MonoRunner.inst.sendStatus(0, b);
-    }
 
     static void copyAssetDir(AssetManager am, String path, String outpath) {
         try {
@@ -72,7 +70,7 @@ public class MonoRunner extends Instrumentation
                 String toFile = outpath + "/" + res[i];
                 try {
                     InputStream fromStream = am.open(fromFile);
-                    Log.w("MONO", "\tCOPYING " + fromFile + " to " + toFile);
+                    Log.w("DOTNET", "\tCOPYING " + fromFile + " to " + toFile);
                     copy(fromStream, new FileOutputStream(toFile));
                 } catch (FileNotFoundException e) {
                     new File(toFile).mkdirs();
@@ -82,7 +80,7 @@ public class MonoRunner extends Instrumentation
             }
         }
         catch (Exception e) {
-            Log.w("MONO", "EXCEPTION", e);
+            Log.w("DOTNET", "EXCEPTION", e);
         }
     }
 
@@ -94,5 +92,5 @@ public class MonoRunner extends Instrumentation
         out.close();
     }
 
-    native int initRuntime(String libsDir, String cacheDir);
+    native int initRuntime(String libsDir, String cacheDir, String docsDir);
 }
index a2bc8f1..8cfdaaf 100644 (file)
@@ -23,8 +23,8 @@
 
 static char *bundle_path;
 
-#define LOG_INFO(fmt, ...) __android_log_print(ANDROID_LOG_DEBUG, "MONO", fmt, ##__VA_ARGS__)
-#define LOG_ERROR(fmt, ...) __android_log_print(ANDROID_LOG_ERROR, "MONO", fmt, ##__VA_ARGS__)
+#define LOG_INFO(fmt, ...) __android_log_print(ANDROID_LOG_DEBUG, "DOTNET", fmt, ##__VA_ARGS__)
+#define LOG_ERROR(fmt, ...) __android_log_print(ANDROID_LOG_ERROR, "DOTNET", fmt, ##__VA_ARGS__)
 
 static MonoAssembly*
 load_assembly (const char *name, const char *culture)
@@ -185,15 +185,18 @@ strncpy_str (JNIEnv *env, char *buff, jstring str, int nbuff)
 }
 
 int
-Java_net_dot_MonoRunner_initRuntime (JNIEnv* env, jobject thiz, jstring j_files_dir, jstring j_cache_dir)
+Java_net_dot_MonoRunner_initRuntime (JNIEnv* env, jobject thiz, jstring j_files_dir, jstring j_cache_dir, jstring j_docs_dir)
 {
     char file_dir[2048];
     char cache_dir[2048];
+    char docs_dir[2048];
     strncpy_str (env, file_dir, j_files_dir, sizeof(file_dir));
     strncpy_str (env, cache_dir, j_cache_dir, sizeof(cache_dir));
+    strncpy_str (env, docs_dir, j_docs_dir, sizeof(docs_dir));
 
     bundle_path = file_dir;
     setenv ("HOME", bundle_path, true);
     setenv ("TMPDIR", cache_dir, true); 
+    setenv ("DOCSDIR", docs_dir, true); 
     return mono_mobile_runtime_init ();
 }
index 9f69be4..26429be 100644 (file)
@@ -53,10 +53,7 @@ public class SimpleAndroidTestRunner : AndroidApplicationEntryPoint, IDevice
         }
     }
 
-    protected override void TerminateWithSuccess()
-    {
-        Console.WriteLine("[TerminateWithSuccess]");
-    }
+    protected override void TerminateWithSuccess() {}
 
     private int? _maxParallelThreads;
 
@@ -84,6 +81,15 @@ public class SimpleAndroidTestRunner : AndroidApplicationEntryPoint, IDevice
 
     public override TextWriter? Logger => null;
 
-    public override string TestsResultsFinalPath => 
-        Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "testResults.xml");
+    public override string TestsResultsFinalPath
+    {
+        get
+        {
+            string? publicDir = Environment.GetEnvironmentVariable("DOCSDIR");
+            if (string.IsNullOrEmpty(publicDir))
+                throw new ArgumentException("DOCSDIR should not be empty");
+
+            return Path.Combine(publicDir, "testResults.xml");
+        }
+    }
 }