Skip to content

Commit

Permalink
Merge pull request #93 from Malinskiy/fix/test-runner-stdout
Browse files Browse the repository at this point in the history
fix(adam): test runner should read only stdout if possible
  • Loading branch information
Malinskiy committed Feb 1, 2023
2 parents 27e509b + cbb1d47 commit ba9adbc
Show file tree
Hide file tree
Showing 20 changed files with 627 additions and 156 deletions.
14 changes: 12 additions & 2 deletions .github/workflows/ci.yaml
Expand Up @@ -13,6 +13,11 @@ jobs:
- uses: malinskiy/action-android/install-sdk@release/0.1.1
- name: build & test
run: ./gradlew assemble test jacocoTestReport
- name: Publish Test Report
uses: mikepenz/action-junit-report@v3
if: always()
with:
report_paths: '**/build/test-results/test/TEST-*.xml'
- name: archive test results
if: failure()
run: (cd adam/build/reports/tests/test; zip -r -X ../../../../../test-result.zip .)
Expand All @@ -38,7 +43,7 @@ jobs:
runs-on: macOS-10.15
strategy:
matrix:
api: [ 21, 22, 23, 24, 25, 26, 27, 28, 29, 30 ]
api: [ 21, 22, 23, 24, 25, 26, 28, 29, 30, 31, 33 ]
steps:
- uses: actions/checkout@v1
- name: Set up JDK 11
Expand All @@ -55,7 +60,12 @@ jobs:
cmdOptions: -no-snapshot-save -noaudio -no-boot-anim -cores 2 -memory 3072 -no-window -gpu swiftshader_indirect
api: ${{ matrix.api }}
tag: google_apis
abi: x86
abi: x86_64
- name: Publish Test Report
uses: mikepenz/action-junit-report@v3
if: always()
with:
report_paths: '**/build/test-results/integrationTest/TEST-*.xml'
- name: Generate integration code coverage report
run: ./gradlew :adam:jacocoIntegrationTestReport
- name: archive integration test results
Expand Down
Expand Up @@ -22,6 +22,7 @@ import assertk.assertions.hasSize
import assertk.assertions.isEqualTo
import assertk.assertions.isNotEqualTo
import assertk.assertions.startsWith
import com.malinskiy.adam.Const
import com.malinskiy.adam.request.device.FetchDeviceFeaturesRequest
import com.malinskiy.adam.request.device.ListDevicesRequest
import com.malinskiy.adam.request.framebuffer.RawImageScreenCaptureAdapter
Expand Down Expand Up @@ -76,7 +77,12 @@ class E2ETest {
ShellCommandRequest("echo hello"),
adbRule.deviceSerial
)
assertThat(response).isEqualTo(ShellCommandResult("hello${adbRule.lineSeparator}", 0))
assertThat(response).isEqualTo(
ShellCommandResult(
"hello${adbRule.lineSeparator}".toByteArray(Const.DEFAULT_TRANSPORT_ENCODING),
0
)
)
}
}

Expand Down
Expand Up @@ -19,6 +19,7 @@ package com.malinskiy.adam.integration
import assertk.assertThat
import assertk.assertions.isEmpty
import assertk.assertions.isEqualTo
import com.malinskiy.adam.Const
import com.malinskiy.adam.request.Feature
import com.malinskiy.adam.request.shell.v2.ChanneledShellCommandRequest
import com.malinskiy.adam.request.shell.v2.ShellCommandInputChunk
Expand All @@ -41,8 +42,8 @@ class ShellV2E2ETest {
fun testDefault() = runBlocking {
val result = adbRule.adb.execute(ShellCommandRequest("echo foo; echo bar >&2; exit 17"), adbRule.deviceSerial)
assertThat(result.exitCode).isEqualTo(17)
assertThat(result.stdout).isEqualTo("foo\n")
assertThat(result.stderr).isEqualTo("bar\n")
assertThat(result.output).isEqualTo("foo\n")
assertThat(result.errorOutput).isEqualTo("bar\n")
}

@Test
Expand All @@ -53,7 +54,7 @@ class ShellV2E2ETest {
val stdioJob = launch(Dispatchers.IO) {
stdio.send(
ShellCommandInputChunk(
stdin = "cafebabe"
stdin = "cafebabe".toByteArray(Const.DEFAULT_TRANSPORT_ENCODING)
)
)

Expand All @@ -68,8 +69,8 @@ class ShellV2E2ETest {
val stderrBuilder = StringBuilder()
var exitCode = 1
for (i in receiveChannel) {
i.stdout?.let { stdoutBuilder.append(it) }
i.stderr?.let { stderrBuilder.append(it) }
i.stdout?.let { stdoutBuilder.append(String(it, Const.DEFAULT_TRANSPORT_ENCODING)) }
i.stderr?.let { stderrBuilder.append(String(it, Const.DEFAULT_TRANSPORT_ENCODING)) }
i.exitCode?.let { exitCode = it }
}
stdioJob.join()
Expand Down
Expand Up @@ -78,9 +78,11 @@ class TestRunnerE2ETest {
"com.example.test",
InstrumentOptions(
clazz = listOf("com.example.AbstractFailingTest")
)
), serial = rule.deviceSerial,
scope = this
),
rule.supportedFeatures,
this
),
serial = rule.deviceSerial,
)

val events = mutableListOf<TestEvent>()
Expand Down Expand Up @@ -113,9 +115,11 @@ class TestRunnerE2ETest {
InstrumentOptions(
clazz = listOf("com.example.AbstractFailingTest")
),
protobuf = true
), serial = rule.deviceSerial,
scope = this
rule.supportedFeatures,
this,
protobuf = true,
),
serial = rule.deviceSerial,
)

val events = mutableListOf<TestEvent>()
Expand Down
Expand Up @@ -35,7 +35,7 @@ class AbbE2ETest {
fun testStreamingInstallRequest() {
runBlocking {
var result = adbRule.adb.execute(AbbRequest(listOf("-l"), adbRule.supportedFeatures), serial = adbRule.deviceSerial)
assertThat(result.stdout).startsWith("Currently running services:")
assertThat(result.output).startsWith("Currently running services:")
}
}
}
@@ -0,0 +1,79 @@
/*
* Copyright (C) 2023 Anton Malinskiy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.malinskiy.adam.request.shell

import com.malinskiy.adam.AndroidDebugBridgeClient
import com.malinskiy.adam.Const
import com.malinskiy.adam.request.Feature
import com.malinskiy.adam.request.MultiRequest
import com.malinskiy.adam.request.NonSpecifiedTarget
import com.malinskiy.adam.request.Target
import com.malinskiy.adam.request.shell.v1.ChanneledShellCommandRequest
import com.malinskiy.adam.request.shell.v2.ShellCommandInputChunk
import com.malinskiy.adam.request.shell.v2.ShellCommandResultChunk
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.channels.produce
import com.malinskiy.adam.request.shell.v2.ChanneledShellCommandRequest as V2ChanneledShellCommandRequest

abstract class AsyncCompatShellCommandRequest<T : Any>(
val cmd: String,
private val supportedFeatures: List<Feature>,
private val target: Target = NonSpecifiedTarget,
private val coroutineScope: CoroutineScope,
private val socketIdleTimeout: Long? = null,
) : MultiRequest<ReceiveChannel<T>>() {

abstract suspend fun convertChunk(response: ShellCommandResultChunk): T?

override suspend fun execute(
androidDebugBridgeClient: AndroidDebugBridgeClient,
serial: String?
): ReceiveChannel<T> {
return when {
supportedFeatures.contains(Feature.SHELL_V2) -> {
val channel = Channel<ShellCommandInputChunk>()
val receiveChannel = androidDebugBridgeClient.execute(
V2ChanneledShellCommandRequest(cmd, channel, target, socketIdleTimeout), coroutineScope, serial,
)
coroutineScope.produce {
for (chunk in receiveChannel) {
convertChunk(chunk)?.let { send(it) }
}
this@AsyncCompatShellCommandRequest.close(this.channel)
}
}

else -> {
val receiveChannel: ReceiveChannel<String> = androidDebugBridgeClient.execute(
ChanneledShellCommandRequest(cmd, target, socketIdleTimeout), coroutineScope, serial,
)
coroutineScope.produce {
for (line in receiveChannel) {
val chunk = ShellCommandResultChunk(stdout = line.toByteArray(Const.DEFAULT_TRANSPORT_ENCODING))
convertChunk(chunk)?.let { send(it) }
}
this@AsyncCompatShellCommandRequest.close(this.channel)
}
}
}
}

abstract suspend fun close(channel: SendChannel<T>)
}
Expand Up @@ -16,7 +16,31 @@

package com.malinskiy.adam.request.shell.v1

import com.malinskiy.adam.Const

data class ShellCommandResult(
val output: String,
val stdout: ByteArray,
val exitCode: Int
)
) {
val output: String by lazy {
String(stdout, Const.DEFAULT_TRANSPORT_ENCODING)
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as ShellCommandResult

if (!stdout.contentEquals(other.stdout)) return false
if (exitCode != other.exitCode) return false

return true
}

override fun hashCode(): Int {
var result = stdout.contentHashCode()
result = 31 * result + exitCode
return result
}
}
Expand Up @@ -16,20 +16,20 @@

package com.malinskiy.adam.request.shell.v1

import com.google.common.io.ByteStreams
import com.malinskiy.adam.Const
import com.malinskiy.adam.exception.RequestRejectedException
import com.malinskiy.adam.request.transform.ResponseTransformer

class ShellResultResponseTransformer : ResponseTransformer<ShellCommandResult> {
override suspend fun process(bytes: ByteArray, offset: Int, limit: Int) {
val part = String(bytes, 0, limit, Const.DEFAULT_TRANSPORT_ENCODING)
builder.append(part)
builder.write(bytes, 0, limit)
}

private val builder = StringBuilder()
private val builder = ByteStreams.newDataOutput()

override fun transform(): ShellCommandResult {
val output = builder.toString()
val output = String(builder.toByteArray(), Const.DEFAULT_TRANSPORT_ENCODING)
val indexOfDelimiter = output.lastIndexOf(SyncShellCommandRequest.EXIT_CODE_DELIMITER)
if (indexOfDelimiter == -1) {
throw RequestRejectedException("No exit code delimiter found in $output")
Expand All @@ -38,8 +38,8 @@ class ShellResultResponseTransformer : ResponseTransformer<ShellCommandResult> {
val exitCodeString = output.substring(indexOfDelimiter + 1).trim()
val exitCode = exitCodeString.toIntOrNull() ?: throw RequestRejectedException("Unexpected exit code value $exitCodeString")
return ShellCommandResult(
output = stdout,
stdout = stdout.toByteArray(Const.DEFAULT_TRANSPORT_ENCODING),
exitCode = exitCode
)
}
}
}

0 comments on commit ba9adbc

Please sign in to comment.