mirror of
https://github.com/python/cpython.git
synced 2026-05-04 09:31:02 +00:00
Merge 231c3a82b3 into 68fe899feb
This commit is contained in:
commit
c34ed06c86
5 changed files with 145 additions and 8 deletions
|
|
@ -0,0 +1 @@
|
|||
The Android testbed can now copy files back from the emulator to the build machine.
|
||||
|
|
@ -161,6 +161,10 @@ ### Testing a third-party package
|
|||
directory.
|
||||
* `--site-packages`: the directory to copy into the testbed app to use as site
|
||||
packages.
|
||||
* `--pull`: if specified, the testbed app will pull the file or folder from the device
|
||||
back to the build machine after the test run. This is useful for retrieving coverage
|
||||
data, for example. Can be used multiple times.
|
||||
* `--output-dir`: the directory on the build machine to which files will be pulled.
|
||||
|
||||
Extra arguments on the `android.py test` command line will be passed through to
|
||||
Python – use `--` to separate them from `android.py`'s own options. You must include
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
import subprocess
|
||||
import sys
|
||||
import sysconfig
|
||||
import zipfile
|
||||
from asyncio import wait_for
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime, timezone
|
||||
|
|
@ -635,6 +636,14 @@ def stop_app(serial):
|
|||
run([adb, "-s", serial, "shell", "am", "force-stop", APP_ID], log=False)
|
||||
|
||||
|
||||
def _extract_output_archives(output_dir):
|
||||
"""Extract all zip archives written by PythonSuite.kt and delete them."""
|
||||
for zip_path in Path(output_dir).glob("*.zip"):
|
||||
with zipfile.ZipFile(zip_path) as zf:
|
||||
zf.extractall(output_dir)
|
||||
zip_path.unlink()
|
||||
|
||||
|
||||
async def gradle_task(context):
|
||||
env = os.environ.copy()
|
||||
if context.managed:
|
||||
|
|
@ -669,10 +678,18 @@ async def gradle_task(context):
|
|||
for name, value in [
|
||||
("python.sitePackages", context.site_packages),
|
||||
("python.cwd", context.cwd),
|
||||
(
|
||||
"python.outputDir",
|
||||
context.output_dir
|
||||
),
|
||||
(
|
||||
"android.testInstrumentationRunnerArguments.pythonArgs",
|
||||
json.dumps(context.args),
|
||||
),
|
||||
(
|
||||
"android.testInstrumentationRunnerArguments.pythonPull",
|
||||
json.dumps(context.pull) if context.pull else None,
|
||||
),
|
||||
]
|
||||
if value
|
||||
]
|
||||
|
|
@ -695,6 +712,8 @@ async def gradle_task(context):
|
|||
|
||||
status = await wait_for(process.wait(), timeout=1)
|
||||
if status == 0:
|
||||
if context.pull and context.output_dir:
|
||||
_extract_output_archives(Path(context.output_dir))
|
||||
exit(0)
|
||||
else:
|
||||
raise CalledProcessError(status, args)
|
||||
|
|
@ -705,6 +724,9 @@ async def gradle_task(context):
|
|||
|
||||
|
||||
async def run_testbed(context):
|
||||
if context.pull and not context.output_dir:
|
||||
sys.exit("--output-dir is required when --pull is used.")
|
||||
|
||||
setup_ci()
|
||||
setup_sdk()
|
||||
setup_testbed()
|
||||
|
|
@ -975,6 +997,15 @@ def add_parser(*args, **kwargs):
|
|||
test.add_argument(
|
||||
"--cwd", metavar="DIR", type=abspath,
|
||||
help="Directory to copy as the app's working directory.")
|
||||
test.add_argument(
|
||||
"--pull", metavar="PATH", action="append", default=[],
|
||||
help="File or directory to copy from the app's working directory back "
|
||||
"to the host after the test run. Paths are relative to --cwd on the "
|
||||
"device. May be given multiple times.")
|
||||
test.add_argument(
|
||||
"--output-dir", metavar="DIR", type=abspath,
|
||||
help="Local directory to write files pulled via --pull. "
|
||||
"Required when --pull is used.")
|
||||
test.add_argument(
|
||||
"args", nargs="*", help=f"Python command-line arguments. "
|
||||
f"Separate them from {SCRIPT_NAME}'s own arguments with `--`. "
|
||||
|
|
|
|||
|
|
@ -180,12 +180,18 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
// If the previous test run succeeded and nothing has changed,
|
||||
// Gradle thinks there's no need to run it again. Override that.
|
||||
afterEvaluate {
|
||||
(localDevices.names + listOf("connected")).forEach {
|
||||
tasks.named("${it}DebugAndroidTest") {
|
||||
(localDevices.names + listOf("connected")).forEach { deviceName ->
|
||||
val copyOutputTask = createCopyOutputTask(deviceName)
|
||||
tasks.named("${deviceName}DebugAndroidTest") {
|
||||
// If the previous test run succeeded and nothing has changed,
|
||||
// Gradle thinks there's no need to run it again. Override that.
|
||||
outputs.upToDateWhen { false }
|
||||
|
||||
// If python.outputDir is set, copy all files that are pulled
|
||||
// from the emulator to the host by the UTP to the given output
|
||||
// directory on the host.
|
||||
copyOutputTask?.let { finalizedBy(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -334,6 +340,43 @@ abstract class CreateEmulatorTask : DefaultTask() {
|
|||
}
|
||||
|
||||
|
||||
fun createCopyOutputTask(deviceName: String): TaskProvider<Copy>? {
|
||||
val outputDir = findProperty("python.outputDir") as String?
|
||||
if (outputDir.isNullOrEmpty()) return null
|
||||
|
||||
val additionalOutputPath = if (deviceName == "connected") {
|
||||
"outputs/connected_android_test_additional_output"
|
||||
} else {
|
||||
"outputs/managed_device_android_test_additional_output"
|
||||
}
|
||||
|
||||
// PythonSuite.kt packs all output files into a single zip archive,
|
||||
// to avoid issues because the UTP copy skips dotfiles like ".coverage".
|
||||
val archiveName = "org.python.testbed-output.zip"
|
||||
|
||||
return tasks.register<Copy>("${deviceName}CopyTestOutput") {
|
||||
from(layout.buildDirectory.dir(additionalOutputPath))
|
||||
// The subfolders of `connected_android_test_additional_output` contains
|
||||
// names that are not equal to the serial of the device.
|
||||
// The subfolders of `managed_device_android_test_additional_output` are
|
||||
// also unpredictable, because e.g. the subfolder for the "maxVersion" emulator
|
||||
// is named "minVersion".
|
||||
// So we can't rely on the subfolder names and search for the archive in
|
||||
// all subfolders. The archive should be in exactly one of the subfolders.
|
||||
include("**/$archiveName")
|
||||
into(outputDir)
|
||||
// Flatten: drop any device-subfolder prefix, put the zip
|
||||
// directly in outputDir.
|
||||
eachFile { path = name }
|
||||
includeEmptyDirs = false
|
||||
// Each android.py invocation runs only one device at a time,
|
||||
// so there should never be more than one archive. Fail loudly
|
||||
// if that assumption is violated.
|
||||
duplicatesStrategy = DuplicatesStrategy.FAIL
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Create some custom tasks to copy Python and its standard library from
|
||||
// elsewhere in the repository.
|
||||
androidComponents.onVariants { variant ->
|
||||
|
|
|
|||
|
|
@ -1,29 +1,39 @@
|
|||
package org.python.testbed
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.test.annotation.UiThreadTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.json.JSONArray
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
import java.io.File
|
||||
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class PythonSuite {
|
||||
@Test
|
||||
@UiThreadTest
|
||||
fun testPython() {
|
||||
val instrumentation = InstrumentationRegistry.getInstrumentation()
|
||||
val args = InstrumentationRegistry.getArguments()
|
||||
val start = System.currentTimeMillis()
|
||||
try {
|
||||
val status = PythonTestRunner(
|
||||
InstrumentationRegistry.getInstrumentation().targetContext
|
||||
).run(
|
||||
InstrumentationRegistry.getArguments().getString("pythonArgs")!!,
|
||||
val status = PythonTestRunner(instrumentation.targetContext).run(
|
||||
args.getString("pythonArgs")!!,
|
||||
)
|
||||
assertEquals(0, status)
|
||||
} finally {
|
||||
// Copy files requested via --pull to the directory that AGP/UTP
|
||||
// injected as `additionalTestOutputDir`. AGP will pull everything
|
||||
// written there back to the host before shutting down the emulator.
|
||||
copyOutputFiles(instrumentation.targetContext, args)
|
||||
|
||||
// Make sure the process lives long enough for the test script to
|
||||
// detect it (see `find_pid` in android.py).
|
||||
val delay = 2000 - (System.currentTimeMillis() - start)
|
||||
|
|
@ -32,4 +42,52 @@ class PythonSuite {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyOutputFiles(context: Context, args: Bundle) {
|
||||
// A list of file paths (relative to the Python working directory) that should be
|
||||
// copied back to the host after the test finishes.
|
||||
val pullPathsJson = args.getString("pythonPull") ?: return
|
||||
|
||||
// The output directory is created by AGP/UTP and points to a location inside
|
||||
// the emulator's filesystem. AGP/UTP will pull everything from there back
|
||||
// to the host after the test finishes.
|
||||
val outputDir = args.getString("additionalTestOutputDir") ?: return
|
||||
|
||||
// Pack all files into a single zip archive to avoid issues because the UTP copy
|
||||
// skips dotfiles like ".coverage".
|
||||
val archiveFile = File(outputDir, "org.python.testbed-output.zip")
|
||||
val srcBase = File(context.filesDir, "python/cwd")
|
||||
val paths = JSONArray(pullPathsJson)
|
||||
java.util.zip.ZipOutputStream(archiveFile.outputStream().buffered()).use { zip ->
|
||||
for (i in 0 until paths.length()) {
|
||||
val src = File(srcBase, paths.getString(i))
|
||||
if (!src.exists()) {
|
||||
android.util.Log.w("python.stderr", "Pull path not found: $src\n")
|
||||
continue
|
||||
}
|
||||
try {
|
||||
addToZip(zip, src, src.name)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("python.stderr", "Failed to zip $src: $e\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
android.util.Log.i("python.stdout", "Created output archive: $archiveFile\n")
|
||||
}
|
||||
|
||||
private fun addToZip(
|
||||
zip: java.util.zip.ZipOutputStream,
|
||||
file: File,
|
||||
entryName: String,
|
||||
) {
|
||||
if (file.isDirectory) {
|
||||
for (child in file.listFiles() ?: emptyArray()) {
|
||||
addToZip(zip, child, "$entryName/${child.name}")
|
||||
}
|
||||
} else {
|
||||
zip.putNextEntry(java.util.zip.ZipEntry(entryName))
|
||||
file.inputStream().use { it.copyTo(zip) }
|
||||
zip.closeEntry()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue