Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Shards and Tests count do not match #1059

Merged
merged 27 commits into from
Sep 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b476766
Initial test
pawelpasterz Aug 26, 2020
46a21e2
Change TestFlankMethod
pawelpasterz Aug 26, 2020
c5f020c
Modify args
pawelpasterz Aug 26, 2020
68151a1
Changes in shard
pawelpasterz Aug 26, 2020
784b638
Modify context
pawelpasterz Aug 26, 2020
8f762fe
Platform
pawelpasterz Aug 26, 2020
74e8a6d
Turn off tests
pawelpasterz Aug 26, 2020
572deb1
Revert "Turn off tests"
pawelpasterz Aug 27, 2020
e44e5f1
Make tests compile again
pawelpasterz Aug 27, 2020
57fd995
Extract Chunk into separate file
pawelpasterz Aug 27, 2020
cd4ee4e
Update tests
pawelpasterz Aug 27, 2020
776fcb5
Linting
pawelpasterz Aug 27, 2020
db6aa06
Refactor names
pawelpasterz Aug 27, 2020
6dbbf62
Implement missing tests
pawelpasterz Aug 27, 2020
e46b623
reorder params in asserts
adamfilipow92 Aug 27, 2020
9b7285b
Update BeforeRunMessage.kt
adamfilipow92 Aug 27, 2020
336469d
Create 986_test_count_and_smart_sharding_ count_dont_match.md
adamfilipow92 Aug 28, 2020
eff2f90
Update 986_test_count_and_smart_sharding_ count_dont_match.md
adamfilipow92 Aug 28, 2020
3eaa51a
default class test time
adamfilipow92 Aug 28, 2020
1950b2d
added command for class timeout and fix some tests
adamfilipow92 Sep 1, 2020
a0c84ad
resolve conflicts
adamfilipow92 Sep 2, 2020
ac90cc2
update tests
adamfilipow92 Sep 2, 2020
b7da950
update docs and add tests for test time
adamfilipow92 Sep 2, 2020
5defcf8
small refractor
adamfilipow92 Sep 2, 2020
9dd3587
Address review comments
pawelpasterz Sep 7, 2020
e1e149b
Update tests
pawelpasterz Sep 7, 2020
2e7328a
Update doc
pawelpasterz Sep 7, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions docs/bugs/986_test_count_and_smart_sharding_ count_dont_match.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Test count and smart sharding count don't match

Bug reported in [this issue](https://github.com/Flank/flank/issues/986)

## The problem

Flank does not support parameterized tests sharding. Every class with parameterized is considered as one test during shard calculation.

Flank is using [DEX parser](https://github.com/linkedin/dex-test-parser) to decompile apks and gather info about all the tests inside. As for now, Flank is unable to determine how many times a test in a parameterized class is invoked. Due to this fact scans apks for any class with an annotation that contains `JUnitParamsRunner` or `Parameterized`:

```kotlin

@RunWith(JUnitParamsRunner::class)
...
@RunWith(Parameterized::class)

```

## Solution

1. Flank knows how many tests and classes are being sent to Firebase. So we can inform the user of how many classes we have. Example:

```txt

Smart Flank cache hit: 0% (0 / 9)
Shard times: 240s, 240s, 240s, 360s

Uploading app-debug.apk .
Uploading app-multiple-flaky-debug-androidTest.apk .
5 tests + 4 parameterized classes / 4 shards

```

1. Default test time for classes should be different from the default time for test
1. You can set default test time for class with ```--default-class-test-time``` command
2. If you did not set this time, the default value is 240s
21 changes: 8 additions & 13 deletions test_runner/src/main/kotlin/ftl/args/ArgsHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ import ftl.config.FtlConstants.useMock
import ftl.gc.GcStorage
import ftl.gc.GcToolResults
import ftl.reports.xml.model.JUnitTestResult
import ftl.shard.StringShards
import ftl.shard.createShardsByShardCount
import ftl.shard.shardCountByTime
import ftl.shard.stringShards
import ftl.run.exception.FlankConfigurationError
import ftl.run.exception.FlankGeneralError
import ftl.shard.Chunk
import ftl.shard.TestMethod
import ftl.util.FlankTestMethod
import ftl.util.assertNotEmpty
import java.io.File
Expand Down Expand Up @@ -222,30 +222,25 @@ object ArgsHelper {
): CalculateShardsResult {
if (filteredTests.isEmpty()) {
// Avoid unnecessary computing if we already know there aren't tests to run.
return CalculateShardsResult(listOf(emptyList()), emptyList())
return CalculateShardsResult(emptyList(), emptyList())
}
val (ignoredTests, testsToExecute) = filteredTests.partition { it.ignored }
val shards = if (args.disableSharding) {
// Avoid to cast it to MutableList<String> to be agnostic from the caller.
listOf(testsToExecute.map { it.testName }.toMutableList())
listOf(Chunk(testsToExecute.map { TestMethod(name = it.testName, isParameterized = it.isParameterizedClass, time = 0.0) }))
} else {
val oldTestResult = GcStorage.downloadJunitXml(args) ?: JUnitTestResult(mutableListOf())
val shardCount = forcedShardCount ?: shardCountByTime(testsToExecute, oldTestResult, args)
createShardsByShardCount(testsToExecute, oldTestResult, args, shardCount).stringShards()
createShardsByShardCount(testsToExecute, oldTestResult, args, shardCount).map { Chunk(it.testMethods) }
}

return CalculateShardsResult(testMethodsAlwaysRun(shards, args), ignoredTestCases = ignoredTests.map { it.testName })
}

private fun testMethodsAlwaysRun(shards: StringShards, args: IArgs): StringShards {
private fun testMethodsAlwaysRun(shards: List<Chunk>, args: IArgs): List<Chunk> {
val alwaysRun = args.testTargetsAlwaysRun
val find = shards.flatMap { it.testMethods }.filter { alwaysRun.contains(it.name) }

shards.forEach { shard ->
shard.removeAll(alwaysRun)
shard.addAll(0, alwaysRun)
}

return shards
return shards.map { Chunk(find + it.testMethods.filterNot { method -> find.contains(method) }) }
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package ftl.args

data class CalculateShardsResult(val shardChunks: ShardChunks, val ignoredTestCases: IgnoredTestCases)
import ftl.shard.Chunk

data class CalculateShardsResult(val shardChunks: List<Chunk>, val ignoredTestCases: IgnoredTestCases)
1 change: 1 addition & 0 deletions test_runner/src/main/kotlin/ftl/args/CommonArgs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ data class CommonArgs(
override val outputStyle: OutputStyle,
override val disableResultsUpload: Boolean,
override val defaultTestTime: Double,
override val defaultClassTestTime: Double,
override val useAverageTestTimeForNewTests: Boolean,
) : IArgs
1 change: 1 addition & 0 deletions test_runner/src/main/kotlin/ftl/args/CreateCommonArgs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ fun CommonConfig.createCommonArgs(
localResultDir = flank.localResultsDir!!,
disableResultsUpload = flank.disableResultsUpload!!,
defaultTestTime = flank.defaultTestTime!!,
defaultClassTestTime = flank.defaultClassTestTime!!,
useAverageTestTimeForNewTests = flank.useAverageTestTimeForNewTests!!
).apply {
ArgsHelper.createJunitBucket(project, smartFlankGcsPath)
Expand Down
1 change: 1 addition & 0 deletions test_runner/src/main/kotlin/ftl/args/IArgs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ interface IArgs {
get() = maxTestShards in AVAILABLE_VIRTUAL_SHARD_COUNT_RANGE

val defaultTestTime: Double
val defaultClassTestTime: Double
val useAverageTestTimeForNewTests: Boolean

fun useLocalResultDir() = localResultDir != defaultLocalResultsDir
Expand Down
5 changes: 3 additions & 2 deletions test_runner/src/main/kotlin/ftl/args/IosArgs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ftl.args
import com.google.common.annotations.VisibleForTesting
import ftl.ios.Xctestrun.findTestNames
import ftl.run.exception.FlankConfigurationError
import ftl.shard.Chunk
import ftl.util.FlankTestMethod

data class IosArgs(
Expand All @@ -14,7 +15,7 @@ data class IosArgs(
) : IArgs by commonArgs {

override val useLegacyJUnitResult = true
val testShardChunks: ShardChunks by lazy { calculateShardChunks() }
val testShardChunks: List<Chunk> by lazy { calculateShardChunks() }

companion object : IosArgsCompanion()

Expand Down Expand Up @@ -62,7 +63,7 @@ IosArgs
}

private fun IosArgs.calculateShardChunks() = if (disableSharding)
listOf(emptyList()) else
emptyList() else
ArgsHelper.calculateShards(
filteredTests = filterTests(findTestNames(xctestrunFile), testTargets)
.distinct()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ftl.args.ArgsHelper
import ftl.args.yml.IYmlKeys
import ftl.config.Config
import ftl.config.FtlConstants
import ftl.shard.DEFAULT_CLASS_TEST_TIME_SEC
import ftl.shard.DEFAULT_TEST_TIME_SEC
import picocli.CommandLine

Expand Down Expand Up @@ -148,6 +149,9 @@ data class CommonFlankConfig @JsonIgnore constructor(
@set:JsonProperty("default-test-time")
var defaultTestTime: Double? by data

@set:JsonProperty("default-class-test-time")
var defaultClassTestTime: Double? by data
Copy link
Contributor

@kozaxinan kozaxinan Aug 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does keys list need this new value to be able to be checked by flank doctor? defaultTestTime is also missing in key list.


@set:CommandLine.Option(
names = ["--use-average-test-time-for-new-tests"],
description = ["Enable using average time from previous tests duration when using SmartShard and tests did not run before."]
Expand Down Expand Up @@ -178,6 +182,9 @@ data class CommonFlankConfig @JsonIgnore constructor(
"output-style",
"disable-results-upload",
"full-junit-result",
"local-result-dir",
"default-test-time",
"default-class-test-time",
"local-result-dir"
)

Expand All @@ -201,6 +208,7 @@ data class CommonFlankConfig @JsonIgnore constructor(
outputStyle = null
disableResultsUpload = false
defaultTestTime = DEFAULT_TEST_TIME_SEC
defaultClassTestTime = DEFAULT_CLASS_TEST_TIME_SEC
useAverageTestTimeForNewTests = false
}
}
Expand Down
3 changes: 2 additions & 1 deletion test_runner/src/main/kotlin/ftl/run/DumpShards.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ftl.run.common.prettyPrint
import ftl.run.exception.FlankConfigurationError
import ftl.run.model.AndroidMatrixTestShards
import ftl.run.platform.android.getAndroidMatrixShards
import ftl.shard.testCases
import ftl.util.obfuscatePrettyPrinter
import java.nio.file.Files
import java.nio.file.Paths
Expand Down Expand Up @@ -35,7 +36,7 @@ fun dumpShards(
) {
saveShardChunks(
shardFilePath = shardFilePath,
shards = args.testShardChunks,
shards = args.testShardChunks.testCases,
size = args.testShardChunks.size,
obfuscatedOutput = obfuscatedOutput
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package ftl.run.model

import ftl.shard.Chunk
import ftl.args.IgnoredTestCases
import ftl.args.ShardChunks
import ftl.util.FileReference

sealed class AndroidTestContext

data class InstrumentationTestContext(
val app: FileReference,
val test: FileReference,
val shards: ShardChunks = emptyList(),
val shards: List<Chunk> = emptyList(),
val ignoredTestCases: IgnoredTestCases = emptyList()
) : AndroidTestContext()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import ftl.run.platform.common.afterRunTests
import ftl.run.platform.common.beforeRunMessage
import ftl.run.platform.common.beforeRunTests
import ftl.run.exception.FlankGeneralError
import ftl.shard.Chunk
import ftl.shard.testCases
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
Expand All @@ -31,7 +33,7 @@ internal suspend fun runAndroidTests(args: AndroidArgs): TestResult = coroutineS
// GcAndroidTestMatrix.execute() 3x retry => matrix id (string)
val devices = GcAndroidDevice.build(args.devices)
val testMatrices = mutableListOf<Deferred<TestMatrix>>()
val allTestShardChunks = mutableListOf<List<String>>()
val allTestShardChunks = mutableListOf<Chunk>()
val ignoredTestsShardChunks = mutableListOf<List<String>>()

val history = GcToolResults.createToolResultsHistory(args)
Expand Down Expand Up @@ -64,7 +66,7 @@ internal suspend fun runAndroidTests(args: AndroidArgs): TestResult = coroutineS
println(beforeRunMessage(args, allTestShardChunks))
TestResult(
matrixMap = afterRunTests(testMatrices.awaitAll(), runGcsPath, stopwatch, args),
shardChunks = allTestShardChunks,
shardChunks = allTestShardChunks.testCases,
ignoredTests = ignoredTestsShardChunks.flatten()
)
}
Expand Down
5 changes: 3 additions & 2 deletions test_runner/src/main/kotlin/ftl/run/platform/RunIosTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import ftl.run.model.TestResult
import ftl.run.platform.common.afterRunTests
import ftl.run.platform.common.beforeRunMessage
import ftl.run.platform.common.beforeRunTests
import ftl.shard.testCases
import ftl.util.ShardCounter
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -47,7 +48,7 @@ internal suspend fun runIosTests(iosArgs: IosArgs): TestResult = coroutineScope
iosDeviceList = iosDeviceList,
testZipGcsPath = xcTestGcsPath,
runGcsPath = runGcsPath,
testTargets = testTargets,
testTargets = testTargets.testMethodNames,
xcTestParsed = xcTestParsed,
args = iosArgs,
shardCounter = shardCounter,
Expand All @@ -59,7 +60,7 @@ internal suspend fun runIosTests(iosArgs: IosArgs): TestResult = coroutineScope

TestResult(
matrixMap = afterRunTests(jobs.awaitAll(), runGcsPath, stopwatch, iosArgs),
shardChunks = iosArgs.testShardChunks
shardChunks = iosArgs.testShardChunks.testCases
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ private fun InstrumentationTestContext.calculateShards(
forcedShardCount = args.numUniformShards
).run {
copy(
shards = shardChunks.filter { it.isNotEmpty() },
shards = shardChunks.filter { it.testMethods.isNotEmpty() },
ignoredTestCases = ignoredTestCases
)
}
Expand Down Expand Up @@ -86,7 +86,7 @@ private val ignoredAnnotations = listOf(
"android.support.test.filters.Suppress"
)

private fun String.toFlankTestMethod() = FlankTestMethod("class $this", ignored = false)
private fun String.toFlankTestMethod() = FlankTestMethod("class $this", ignored = false, isParameterizedClass = true)

private fun InstrumentationTestContext.getParametrizedClasses(): List<String> =
DexParser.readDexFiles(test.local).fold(emptyList()) { accumulator, file: DexFile ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ftl.run.platform.android

import ftl.args.AndroidArgs
import ftl.run.model.InstrumentationTestContext
import ftl.shard.testCases

internal fun AndroidArgs.createInstrumentationConfig(
testApk: InstrumentationTestContext
Expand All @@ -12,6 +13,6 @@ internal fun AndroidArgs.createInstrumentationConfig(
orchestratorOption = "USE_ORCHESTRATOR".takeIf { useOrchestrator },
disableSharding = disableSharding,
numUniformShards = numUniformShards,
testShards = testApk.shards,
testShards = testApk.shards.testCases,
keepTestTargetsEmpty = disableSharding && testTargets.isEmpty()
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ftl.args.AndroidArgs
import ftl.run.model.AndroidMatrixTestShards
import ftl.run.model.AndroidTestShards
import ftl.run.model.InstrumentationTestContext
import ftl.shard.testCases

suspend fun AndroidArgs.getAndroidMatrixShards(): AndroidMatrixTestShards = this
.createAndroidTestContexts()
Expand All @@ -15,7 +16,7 @@ private fun List<InstrumentationTestContext>.asMatrixTestShards(): AndroidMatrix
AndroidTestShards(
app = testApks.app.local,
test = testApks.test.local,
shards = testApks.shards.mapIndexed { index, testCases ->
shards = testApks.shards.testCases.mapIndexed { index, testCases ->
"shard-$index" to testCases
}.toMap(),
junitIgnored = testApks.ignoredTestCases
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,46 @@
package ftl.run.platform.common

import ftl.args.IArgs
import ftl.args.ShardChunks
import ftl.config.FtlConstants
import ftl.shard.Chunk
import ftl.shard.TestMethod

internal fun beforeRunMessage(args: IArgs, testShardChunks: ShardChunks): String {
internal fun beforeRunMessage(args: IArgs, testShardChunks: List<Chunk>): String {
val runCount = args.repeatTests
val shardCount = testShardChunks.size
val testsCount = testShardChunks.sumBy { it.size }
val (classesCount, testsCount) = testShardChunks.partitionedTestCases.testAndClassesCount

val result = StringBuilder()
val testString = if (testsCount > 0) "$testsCount test${s(testsCount)}" else ""
val classString = if (classesCount > 0) "$classesCount parameterized class${es(classesCount)}" else ""

result.appendLine(
" $testsCount test${s(testsCount)} / $shardCount shard${s(shardCount)}"
"${FtlConstants.indent}$testString${if (testsCount * classesCount > 0) " + " else ""}$classString / $shardCount shard${s(shardCount)}"
)

if (runCount > 1) {
result.appendLine(" Running ${runCount}x")
val runDevices = runCount * shardCount
val runTests = runCount * testsCount
val runClasses = runCount * classesCount
result.appendLine(" $runDevices total shard${s(runDevices)}")
result.appendLine(" $runTests total test${s(runTests)}")
if (runTests > 0) result.appendLine(" $runTests total test${s(runTests)}")
if (runClasses > 0) result.appendLine(" $runClasses total parameterized class${es(runClasses)}")
}

return result.toString()
}

private fun s(amount: Int): String {
return if (amount > 1) {
"s"
} else {
""
}
}
private val List<Chunk>.partitionedTestCases
get() = flatMap { it.testMethods }.partition { it.isParameterized }

private val Pair<List<TestMethod>, List<TestMethod>>.testAndClassesCount
get() = first.count() to second.count()

private fun s(amount: Int): String =
if (amount > 1) "s"
else ""

private fun es(amount: Int): String =
if (amount > 1) "es"
else ""
9 changes: 9 additions & 0 deletions test_runner/src/main/kotlin/ftl/shard/Chunk.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ftl.shard

data class Chunk(val testMethods: List<TestMethod>) {
val testMethodNames = testMethods.map { it.name }
val size get() = testMethods.size
}

val List<Chunk>.testCases
get() = map { it.testMethodNames }
Loading