Skip to content

Commit 6c68772

Browse files
authored
Merge pull request #889 from MarathonLabs/feature/imporove-max-filename-behaviour
fix(core): add maxFilename limit for outputConfiguration
2 parents d95eeff + 6faa9c5 commit 6c68772

File tree

21 files changed

+246
-129
lines changed

21 files changed

+246
-129
lines changed

configuration/src/main/kotlin/com/malinskiy/marathon/config/OutputConfiguration.kt

+7-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ package com.malinskiy.marathon.config
22

33
import com.fasterxml.jackson.annotation.JsonProperty
44

5+
// Value 0 is equivalent to unlimited path length
6+
const val OUTPUT_MAX_PATH = 0
7+
// Value 0 is equivalent to unlimited filename length
8+
const val OUTPUT_MAX_FILENAME = 255
9+
510
data class OutputConfiguration(
6-
@JsonProperty("maxPath") val maxPath: Int = 255
11+
@JsonProperty("maxPath") val maxPath: Int = OUTPUT_MAX_PATH,
12+
@JsonProperty("maxFilename") val maxFilename: Int = OUTPUT_MAX_FILENAME,
713
)

core/src/main/kotlin/com/malinskiy/marathon/di/Modules.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ val analyticsModule = module {
3838
val coreModule = module {
3939
single {
4040
val configuration = get<Configuration>()
41-
FileManager(configuration.outputConfiguration.maxPath, configuration.outputDir)
41+
FileManager(configuration.outputConfiguration.maxPath, configuration.outputConfiguration.maxFilename, configuration.outputDir)
4242
}
4343
single {
4444
GsonBuilder()

core/src/main/kotlin/com/malinskiy/marathon/io/FileManager.kt

+80-72
Original file line numberDiff line numberDiff line change
@@ -12,97 +12,113 @@ import java.nio.file.Path
1212
import java.nio.file.Paths.get
1313
import java.util.UUID
1414

15+
/**
16+
* Validation logic should check filename first, then check if the resulting path is within max path len
17+
*/
1518
@Suppress("TooManyFunctions")
16-
class FileManager(private val maxPath: Int, private val output: File) {
19+
class FileManager(private val maxPath: Int, private val maxFilename: Int, private val output: File) {
1720
val log = MarathonLogging.logger("FileManager")
1821

19-
fun createFile(fileType: FileType, pool: DevicePoolId, device: DeviceInfo, test: Test, testBatchId: String? = null): File {
20-
val directory = createDirectory(fileType, pool, device)
21-
val filename = createFilename(test, fileType, maxPath - (directory.toAbsolutePath().toString().length + 1), testBatchId)
22+
fun createFile(
23+
fileType: FileType,
24+
pool: DevicePoolId,
25+
device: DeviceInfo,
26+
test: Test? = null,
27+
testBatchId: String? = null,
28+
id: String? = null
29+
): File {
30+
val directory = when {
31+
test != null || testBatchId != null -> createDirectory(fileType, pool, device)
32+
else -> createDirectory(fileType, pool)
33+
}
34+
val filename = when {
35+
test != null -> createTestFilename(test, fileType, testBatchId, id = id)
36+
testBatchId != null -> createBatchFilename(testBatchId, fileType, id = id)
37+
else -> createDeviceFilename(device, fileType, id = id)
38+
}
2239
return createFile(directory, filename)
2340
}
2441

25-
fun createFile(fileType: FileType, pool: DevicePoolId, device: DeviceInfo, testBatchId: String): File {
26-
val directory = createDirectory(fileType, pool, device)
27-
val filename = createFilename(fileType, testBatchId)
28-
return createFile(directory, filename)
29-
}
42+
fun createFolder(folderType: FolderType, pool: DevicePoolId? = null, device: DeviceInfo? = null): File {
43+
var path = get(output.absolutePath, folderType.dir)
44+
if (pool != null) {
45+
path = path.resolve(pool.name)
46+
}
47+
if (device != null) {
48+
path = path.resolve(device.safeSerialNumber)
49+
}
3050

31-
fun createFile(fileType: FileType, pool: DevicePoolId, device: DeviceInfo): File {
32-
val directory = createDirectory(fileType, pool)
33-
val filename = createFilename(device, fileType)
34-
return createFile(directory, filename)
35-
}
51+
val maybeTooLongPath = path.toFile()
52+
path = if (maxPath > 0 && maybeTooLongPath.absolutePath.length > maxPath) {
53+
val trimmed = maybeTooLongPath.absolutePath.take(maxPath)
54+
log.error {
55+
"Directory path length cannot exceed $maxPath characters and has been trimmed from $maybeTooLongPath to $trimmed and can create a conflict. " +
56+
"This happened because the combination of file path, pool name and device serial is too long."
57+
}
58+
File(trimmed)
59+
} else {
60+
maybeTooLongPath
61+
}.toPath()
3662

37-
fun createScreenshotFile(extension: String, pool: DevicePoolId, device: DeviceInfo, test: Test, testBatchId: String): File {
38-
val directory = createDirectory(FileType.SCREENSHOT, pool, device)
39-
val filename =
40-
createFilename(
41-
test,
42-
FileType.SCREENSHOT,
43-
maxPath - (directory.toAbsolutePath().toString().length + 1),
44-
testBatchId = null,
45-
extension,
46-
UUID.randomUUID().toString()
47-
)
48-
return createFile(directory, filename)
63+
return createDirectories(path).toFile()
4964
}
5065

51-
fun createFolder(folderType: FolderType): File = createDirectories(get(output.absolutePath, folderType.dir)).toFile()
52-
fun createFolder(folderType: FolderType, pool: DevicePoolId, device: DeviceInfo): File =
53-
createDirectories(get(output.absolutePath, folderType.dir, pool.name, device.safeSerialNumber)).toFile()
54-
55-
fun createFolder(folderType: FolderType, pool: DevicePoolId): File =
56-
createDirectories(get(output.absolutePath, folderType.dir, pool.name)).toFile()
57-
58-
fun createFolder(folderType: FolderType, device: DeviceInfo): File =
59-
createDirectories(get(output.absolutePath, folderType.dir, device.safeSerialNumber)).toFile()
60-
6166
fun createTestResultFile(filename: String): File {
62-
val resultsFolder = get(output.absolutePath, FileType.TEST_RESULT.dir).toFile()
63-
resultsFolder.mkdirs()
64-
return File(resultsFolder, filename)
67+
val resultsFolder = get(output.absolutePath, FileType.TEST_RESULT.dir)
68+
resultsFolder.toFile().mkdirs()
69+
return createFile(resultsFolder, filename)
6570
}
6671

67-
private fun createDirectory(fileType: FileType, pool: DevicePoolId, device: DeviceInfo): Path =
68-
createDirectories(getDirectory(fileType, pool, device))
69-
70-
private fun createDirectory(fileType: FileType, pool: DevicePoolId): Path =
71-
createDirectories(getDirectory(fileType, pool))
72-
73-
private fun getDirectory(fileType: FileType, pool: DevicePoolId, device: DeviceInfo): Path =
74-
getDirectory(fileType, pool, device.safeSerialNumber)
75-
76-
private fun getDirectory(fileType: FileType, pool: DevicePoolId, serial: String): Path =
77-
get(output.absolutePath, fileType.dir, pool.name, serial)
72+
private fun createDirectory(fileType: FileType, pool: DevicePoolId, device: DeviceInfo? = null): Path {
73+
return createDirectories(getDirectory(fileType, pool, serial = device?.safeSerialNumber))
74+
}
7875

79-
private fun getDirectory(fileType: FileType, pool: DevicePoolId): Path =
80-
get(output.absolutePath, fileType.dir, pool.name)
76+
private fun getDirectory(fileType: FileType, pool: DevicePoolId, serial: String? = null): Path {
77+
val path = get(output.absolutePath, fileType.dir, pool.name)
78+
return serial?.let {
79+
path.resolve(serial)
80+
} ?: path
81+
}
8182

8283
private fun createFile(directory: Path, filename: String): File {
83-
val maybeTooLongPath = File(directory.toFile(), filename)
84-
return if (maybeTooLongPath.absolutePath.length > maxPath) {
84+
val trimmedFilename = if (maxFilename > 0 && filename.length > maxFilename) {
85+
val safeFilename = filename.take(maxFilename)
86+
log.error {
87+
"File name length cannot exceed $maxFilename characters and has been trimmed to $safeFilename and can create a conflict." +
88+
"This usually happens because the test name is too long."
89+
}
90+
safeFilename
91+
} else {
92+
filename
93+
}
94+
val maybeTooLongPath = File(directory.toFile(), trimmedFilename)
95+
return if (maxPath > 0 && maybeTooLongPath.absolutePath.length > maxPath) {
8596
val trimmed = maybeTooLongPath.absolutePath.substring(0 until maxPath)
86-
log.error { "File path length cannot exceed $maxPath characters and has been trimmed to $trimmed and can create a conflict. This happened because the combination of file path, test class name, and test name is too long." }
97+
log.error {
98+
"File path length cannot exceed $maxPath characters and has been trimmed from $maybeTooLongPath to $trimmed and can create a conflict. " +
99+
"This happened because the combination of file path, test class name, and test name is too long."
100+
}
87101
File(trimmed)
88102
} else {
89103
maybeTooLongPath
90104
}
91105
}
92106

93-
private fun createFilename(fileType: FileType, testBatchId: String): String {
107+
private fun createBatchFilename(testBatchId: String, fileType: FileType, id: String? = null): String {
94108
return StringBuilder().apply {
95109
append(testBatchId)
110+
if (id != null) {
111+
append("-$id")
112+
}
96113
if (fileType.suffix.isNotEmpty()) {
97114
append(".$testBatchId")
98115
}
99116
}.toString()
100117
}
101118

102-
private fun createFilename(
119+
private fun createTestFilename(
103120
test: Test,
104121
fileType: FileType,
105-
limit: Int,
106122
testBatchId: String? = null,
107123
overrideExtension: String? = null,
108124
id: String? = null,
@@ -120,24 +136,16 @@ class FileManager(private val maxPath: Int, private val output: File) {
120136
append(".${fileType.suffix}")
121137
}
122138
}.toString()
123-
val rawTestName = test.toTestName().escape()
124-
val testName = when {
125-
limit - testSuffix.length >= 0 -> rawTestName.take(limit - testSuffix.length)
126-
else -> ""
127-
}
128-
val fileName = "$testName$testSuffix"
129-
if (rawTestName.length > testName.length) {
130-
when {
131-
limit >= 0 -> log.error { "File name length cannot exceed $limit characters and has been trimmed to $fileName and can create a conflict. This happened because the combination of file path, test class name, and test name is too long." }
132-
else -> log.error { "Base path for writing a file ${rawTestName}$testSuffix is already maxed out and is ${-limit} characters more than the allowed limit of ${maxPath}." }
133-
}
134-
}
135-
return fileName
139+
val testName = test.toTestName().escape()
140+
return "$testName$testSuffix"
136141
}
137142

138-
private fun createFilename(device: DeviceInfo, fileType: FileType): String {
143+
private fun createDeviceFilename(device: DeviceInfo, fileType: FileType, id: String? = null): String {
139144
return StringBuilder().apply {
140145
append(device.safeSerialNumber)
146+
if (id != null) {
147+
append("-$id")
148+
}
141149
if (fileType.suffix.isNotEmpty()) {
142150
append(".${fileType.suffix}")
143151
}

core/src/main/kotlin/com/malinskiy/marathon/io/FileType.kt

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ enum class FileType(val dir: String, val suffix: String) {
99
VIDEO("video", "mp4"),
1010
SCREENSHOT("screenshot", "gif"),
1111
SCREENSHOT_PNG("screenshot", "png"),
12+
SCREENSHOT_JPG("screenshot", "jpg"),
13+
SCREENSHOT_WEBP("screenshot", "jpg"),
14+
SCREENSHOT_GIF("screenshot", "jpg"),
1215
XCTESTRUN("xctestrun", "xctestrun"),
1316
BILL("bill", "json"),
1417
}

core/src/main/kotlin/com/malinskiy/marathon/report/bill/BillingReporter.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ internal class BillingReporter(
5757

5858
bills.forEach {
5959
val json = gson.toJson(it)
60-
fileManager.createFile(FileType.BILL, it.pool, it.device).writeText(json)
60+
fileManager.createFile(FileType.BILL, it.pool, device = it.device).writeText(json)
6161
}
6262

6363
usageTracker.trackEvent(Event.Devices(bills.size))

core/src/test/kotlin/com/malinskiy/marathon/io/FileManagerTest.kt

+33-11
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,8 @@ import kotlin.io.path.absolutePathString
1616

1717
class FileManagerTest {
1818
private val output = Files.createTempDir()
19-
private val fileManager = FileManager(MAX_PATH, output)
2019

2120
private companion object {
22-
val MAX_PATH = 255
2321
val poolId = DevicePoolId("testPoolId")
2422
val deviceInfo = DeviceInfo(
2523
operatingSystem = OperatingSystem("23"),
@@ -56,30 +54,32 @@ class FileManagerTest {
5654

5755
@Test
5856
fun createFilenameNormalLengthTest() {
57+
val fileManager = FileManager(0, 255, output)
5958
val file = fileManager.createFile(FileType.LOG, poolId, deviceInfo, shortNameTest, batchId)
6059
file.name shouldBeEqualTo "com.example.Clazz#method-batchId.log"
6160
}
6261

6362
@Test
64-
fun createFilenameLongLengthMethodTest() {
63+
fun createFilenameLongLengthMethodLimitedPathTest() {
64+
val fileManager = FileManager(255, 0, output)
6565
val file = fileManager.createFile(FileType.LOG, poolId, deviceInfo, longNameTest, batchId)
6666
file.absolutePath.length shouldBeEqualTo 255
6767
val filenameLimit = 255 - file.parentFile.absolutePath.length - File.separator.length
68-
val fqtnLimit = filenameLimit - "-${batchId}.log".length
69-
file.name shouldBeEqualTo "${longNameTest.toTestName().escape().take(fqtnLimit)}-${batchId}.log"
68+
file.name shouldBeEqualTo "${longNameTest.toTestName()}-${batchId}.log".escape().take(filenameLimit)
7069
}
7170

7271
@Test
7372
fun testCreateFilenameNamedParameterizedLong() {
73+
val fileManager = FileManager(255, 0, output)
7474
val file = fileManager.createFile(FileType.LOG, poolId, deviceInfo, longNamedParameterizedTest, batchId)
7575
file.absolutePath.length shouldBeEqualTo 255
7676
val filenameLimit = 255 - file.parentFile.absolutePath.length - File.separator.length
77-
val fqtnLimit = filenameLimit - "-${batchId}.log".length
78-
file.name shouldBeEqualTo "${longNamedParameterizedTest.toTestName().escape().take(fqtnLimit)}-${batchId}.log"
77+
file.name shouldBeEqualTo "${longNamedParameterizedTest.toTestName()}-${batchId}.log".escape().take(filenameLimit)
7978
}
8079

8180
@Test
8281
fun testDeviceSerialEscaping() {
82+
val fileManager = FileManager(0, 255, output)
8383
val file = fileManager.createFile(
8484
FileType.LOG, poolId, DeviceInfo(
8585
operatingSystem = OperatingSystem("23"),
@@ -95,7 +95,26 @@ class FileManagerTest {
9595
}
9696

9797
@Test
98-
fun testTooLongOutputFolder() {
98+
fun testScreenshotfile() {
99+
val fileManager = FileManager(0, 255, output)
100+
val file = fileManager.createFile(
101+
FileType.SCREENSHOT_PNG, poolId, DeviceInfo(
102+
operatingSystem = OperatingSystem("23"),
103+
serialNumber = "127.0.0.1:5037:emulator-5554",
104+
model = "Android SDK built for x86",
105+
manufacturer = "unknown",
106+
networkState = NetworkState.CONNECTED,
107+
deviceFeatures = listOf(DeviceFeature.SCREENSHOT, DeviceFeature.VIDEO),
108+
healthy = true
109+
),
110+
test = shortNameTest,
111+
id = "on-device-test",
112+
)
113+
file.name shouldBeEqualTo "com.example.Clazz#method-on-device-test.png"
114+
}
115+
116+
@Test
117+
fun testTooLongOutputPathUnlimitedFilename() {
99118
val test = com.malinskiy.marathon.test.Test(
100119
pkg = "com.xxyyzzxxyy.android.abcdefgh.abcdefghi",
101120
clazz = "PackageNameTest",
@@ -105,9 +124,12 @@ class FileManagerTest {
105124

106125
val tempDir = Files.createTempDir()
107126
val proposedPath = Paths.get(tempDir.absolutePath, FileType.LOG.name, poolId.name, deviceInfo.safeSerialNumber)
108-
val additionalPathCharacters = MAX_PATH - proposedPath.absolutePathString().length
127+
val limitedMaxPath = 255
128+
val additionalPathCharacters = limitedMaxPath - proposedPath.absolutePathString().length
109129
val limitedOutputDirectory = File(tempDir, "x".repeat(additionalPathCharacters))
110-
val limitedFileManager = FileManager(MAX_PATH, limitedOutputDirectory)
130+
val limitedFileManager = FileManager(limitedMaxPath, 0, limitedOutputDirectory)
111131
val file = limitedFileManager.createFile(FileType.LOG, poolId, deviceInfo, test, batchId)
112-
}
132+
133+
file.path.length shouldBeEqualTo 255
134+
}
113135
}

0 commit comments

Comments
 (0)