Skip to content

Commit b9f1fba

Browse files
Add upload progress tracking feature to LambdaTest plugin
- Introduced `showUploadProgress` option in tasks and uploaders to enable console progress tracking during file uploads. - Updated `UploaderUtil` to support progress tracking with a new `uploadAndGetId` method. - Implemented `ProgressRequestBody` to handle progress updates and display them in the console. - Modified relevant classes to accommodate the new progress tracking functionality.
1 parent 375c5dc commit b9f1fba

File tree

8 files changed

+225
-11
lines changed

8 files changed

+225
-11
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ runLambdaTest {
3939
isFlutter = true //if you are running flutter dart tests
4040
appId = "lt//1234343" //provide this only if you have already uploaded the app
4141
testSuiteId = "lt//1223444" //provide this only if you have already uploaded the app
42+
showUploadProgress = true //enable upload progress tracking in console
4243
}
4344
```
4445

@@ -50,6 +51,7 @@ uploadApkToLambdaTest {
5051
accessKey = 'yourLambdaTestAccessKey'
5152
appFilePath = 'pathToYourAppFile'
5253
testSuiteFilePath = 'pathToYourTestSuite'
54+
showUploadProgress = true //enable upload progress tracking in console
5355
}
5456
```
5557

@@ -68,6 +70,7 @@ The following capabilities are supported:
6870
- `build`: Set the name of the Espresso test build. Example: My Espresso Build.
6971
- `geoLocation`: Set the geolocation country code if you want to enable the same in your test. Example - FR.
7072
- `tunnel`, `tunnelName`: Set tunnel as true and provide the tunnelName such as NewTunnel as needed if you are running a tunnel.
73+
- `showUploadProgress`: Display real-time upload progress in the console with percentage and data transferred. Example: true.
7174

7275
- `appFilePath` : Path of your app file (this will be uploaded to LambdaTest)
7376

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ plugins {
44
id 'com.diffplug.spotless' version '6.25.0'
55
}
66

7-
version = '1.0.6'
7+
version = '1.0.7'
88
group = 'io.github.lambdatest'
99

1010
repositories {

src/main/java/io/github/lambdatest/gradle/AppUploader.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public class AppUploader {
1919
private String username;
2020
private String accessKey;
2121
private String appFilePath;
22+
private boolean showProgress;
2223

2324
/**
2425
* Creates a new AppUploader instance with the specified credentials and file path.
@@ -28,13 +29,28 @@ public class AppUploader {
2829
* @param appFilePath The path to the application file to be uploaded
2930
*/
3031
public AppUploader(String username, String accessKey, String appFilePath) {
32+
this(username, accessKey, appFilePath, false);
33+
}
34+
35+
/**
36+
* Creates a new AppUploader instance with the specified credentials, file path, and progress
37+
* tracking option.
38+
*
39+
* @param username The LambdaTest account username
40+
* @param accessKey The LambdaTest account access key
41+
* @param appFilePath The path to the application file to be uploaded
42+
* @param showProgress Whether to display upload progress in the console
43+
*/
44+
public AppUploader(
45+
String username, String accessKey, String appFilePath, boolean showProgress) {
3146
if (username == null) throw new IllegalArgumentException("Username cannot be null");
3247
if (accessKey == null) throw new IllegalArgumentException("Access Key cannot be null");
3348
if (appFilePath == null) throw new IllegalArgumentException("App File Path cannot be null");
3449

3550
this.username = username;
3651
this.accessKey = accessKey;
3752
this.appFilePath = appFilePath;
53+
this.showProgress = showProgress;
3854
}
3955

4056
/**
@@ -49,7 +65,8 @@ public CompletableFuture<String> uploadAppAsync() {
4965
() -> {
5066
try {
5167
String appId =
52-
UploaderUtil.uploadAndGetId(username, accessKey, appFilePath);
68+
UploaderUtil.uploadAndGetId(
69+
username, accessKey, appFilePath, showProgress);
5370
logger.info("Uploaded app ID: {}", appId);
5471
return appId;
5572
} catch (IOException e) {

src/main/java/io/github/lambdatest/gradle/LambdaTestTask.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public class LambdaTestTask extends DefaultTask {
4545
private String appId;
4646
private String testSuiteId;
4747
private Integer queueTimeout;
48+
private Boolean showUploadProgress;
4849

4950
/**
5051
* Executes the LambdaTest task, which includes uploading the application and test suite,
@@ -64,16 +65,19 @@ public void runLambdaTest() {
6465
CompletableFuture<String> appIdFuture = null;
6566
CompletableFuture<String> testSuiteIdFuture = null;
6667

68+
boolean progressEnabled = showUploadProgress != null && showUploadProgress;
69+
6770
if (appId == null && appFilePath != null) {
6871
logger.info("Uploading app...");
69-
AppUploader appUploader = new AppUploader(username, accessKey, appFilePath);
72+
AppUploader appUploader =
73+
new AppUploader(username, accessKey, appFilePath, progressEnabled);
7074
appIdFuture = appUploader.uploadAppAsync();
7175
}
7276

7377
if (testSuiteId == null && testSuiteFilePath != null) {
7478
logger.info("Uploading test suite...");
7579
TestSuiteUploader testSuiteUploader =
76-
new TestSuiteUploader(username, accessKey, testSuiteFilePath);
80+
new TestSuiteUploader(username, accessKey, testSuiteFilePath, progressEnabled);
7781
testSuiteIdFuture = testSuiteUploader.uploadTestSuiteAsync();
7882
}
7983

@@ -217,4 +221,8 @@ public void setTestSuiteId(String testSuiteId) {
217221
this.testSuiteId = testSuiteId;
218222
}
219223
}
224+
225+
public void setShowUploadProgress(Boolean showUploadProgress) {
226+
this.showUploadProgress = showUploadProgress;
227+
}
220228
}

src/main/java/io/github/lambdatest/gradle/LambdaUploaderTask.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public class LambdaUploaderTask extends DefaultTask {
2121
private String accessKey;
2222
private String appFilePath;
2323
private String testSuiteFilePath;
24+
private Boolean showUploadProgress;
2425

2526
@TaskAction
2627
public void uploadApkToLambdaTest() {
@@ -31,16 +32,19 @@ public void uploadApkToLambdaTest() {
3132
CompletableFuture<String> testSuiteIdFuture = null;
3233
logger.lifecycle("Starting LambdaTest APK Uploader task...");
3334

35+
boolean progressEnabled = showUploadProgress != null && showUploadProgress;
36+
3437
if (appFilePath != null) {
3538
logger.lifecycle("Uploading app ...");
36-
AppUploader appUploader = new AppUploader(username, accessKey, appFilePath);
39+
AppUploader appUploader =
40+
new AppUploader(username, accessKey, appFilePath, progressEnabled);
3741
appIdFuture = appUploader.uploadAppAsync();
3842
}
3943

4044
if (testSuiteFilePath != null) {
4145
logger.lifecycle("Uploading test suite ...");
4246
TestSuiteUploader testSuiteUploader =
43-
new TestSuiteUploader(username, accessKey, testSuiteFilePath);
47+
new TestSuiteUploader(username, accessKey, testSuiteFilePath, progressEnabled);
4448
testSuiteIdFuture = testSuiteUploader.uploadTestSuiteAsync();
4549
}
4650

@@ -80,4 +84,8 @@ public void setAppFilePath(String appFilePath) {
8084
public void setTestSuiteFilePath(String testSuiteFilePath) {
8185
this.testSuiteFilePath = testSuiteFilePath;
8286
}
87+
88+
public void setShowUploadProgress(Boolean showUploadProgress) {
89+
this.showUploadProgress = showUploadProgress;
90+
}
8391
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package io.github.lambdatest.gradle;
2+
3+
import java.io.File;
4+
import java.io.IOException;
5+
import okhttp3.MediaType;
6+
import okhttp3.RequestBody;
7+
import okio.Buffer;
8+
import okio.BufferedSink;
9+
import okio.ForwardingSink;
10+
import okio.Okio;
11+
import okio.Sink;
12+
13+
/**
14+
* A RequestBody wrapper that tracks upload progress and displays it in the console. This class uses
15+
* OkHttp's native Sink and BufferedSink APIs to monitor the upload progress.
16+
*/
17+
public class ProgressRequestBody extends RequestBody {
18+
19+
private final RequestBody delegate;
20+
private final File file;
21+
private final ProgressCallback progressCallback;
22+
23+
/** Interface for progress callbacks. */
24+
public interface ProgressCallback {
25+
void onProgress(long bytesWritten, long totalBytes, float percentage);
26+
}
27+
28+
/**
29+
* Creates a new ProgressRequestBody that wraps the given RequestBody.
30+
*
31+
* @param delegate The original RequestBody to wrap
32+
* @param file The file being uploaded (used to get total size)
33+
* @param progressCallback Callback to receive progress updates
34+
*/
35+
public ProgressRequestBody(RequestBody delegate, File file, ProgressCallback progressCallback) {
36+
this.delegate = delegate;
37+
this.file = file;
38+
this.progressCallback = progressCallback;
39+
}
40+
41+
@Override
42+
public MediaType contentType() {
43+
return delegate.contentType();
44+
}
45+
46+
@Override
47+
public long contentLength() throws IOException {
48+
return delegate.contentLength();
49+
}
50+
51+
@Override
52+
public void writeTo(BufferedSink sink) throws IOException {
53+
long contentLength = contentLength();
54+
ProgressSink progressSink = new ProgressSink(sink, contentLength, progressCallback);
55+
BufferedSink bufferedSink = Okio.buffer(progressSink);
56+
57+
delegate.writeTo(bufferedSink);
58+
bufferedSink.flush();
59+
}
60+
61+
/**
62+
* Custom Sink implementation that tracks progress while forwarding data to the original sink.
63+
*/
64+
private static class ProgressSink extends ForwardingSink {
65+
private final long totalBytes;
66+
private final ProgressCallback progressCallback;
67+
private long bytesWritten = 0L;
68+
private long lastLoggedPercentage = -1L;
69+
private long lastUpdateTime = System.currentTimeMillis();
70+
71+
public ProgressSink(Sink delegate, long totalBytes, ProgressCallback progressCallback) {
72+
super(delegate);
73+
this.totalBytes = totalBytes;
74+
this.progressCallback = progressCallback;
75+
}
76+
77+
@Override
78+
public void write(Buffer source, long byteCount) throws IOException {
79+
super.write(source, byteCount);
80+
bytesWritten += byteCount;
81+
82+
if (progressCallback != null) {
83+
float percentage = totalBytes > 0 ? (bytesWritten * 100.0f) / totalBytes : 0f;
84+
long currentTime = System.currentTimeMillis();
85+
86+
// Update every 1% or every 250ms for more real-time updates
87+
long currentPercentage = Math.round(percentage);
88+
boolean timeBased = (currentTime - lastUpdateTime) >= 250; // 250ms intervals
89+
90+
if (currentPercentage != lastLoggedPercentage || timeBased) {
91+
progressCallback.onProgress(bytesWritten, totalBytes, percentage);
92+
lastLoggedPercentage = currentPercentage;
93+
lastUpdateTime = currentTime;
94+
}
95+
}
96+
}
97+
}
98+
99+
/**
100+
* Creates a console-based progress callback that displays upload progress.
101+
*
102+
* @param fileName The name of the file being uploaded
103+
* @return A ProgressCallback that logs to console
104+
*/
105+
public static ProgressCallback createConsoleCallback(String fileName) {
106+
return (bytesWritten, totalBytes, percentage) -> {
107+
String formattedBytes = formatBytes(bytesWritten);
108+
String formattedTotal = formatBytes(totalBytes);
109+
110+
System.out.printf(
111+
"\r\u001B[33mUploading %s: %.1f%% (%s / %s)\u001B[0m",
112+
fileName, percentage, formattedBytes, formattedTotal);
113+
System.out.flush();
114+
115+
if (percentage >= 100.0f) {
116+
System.out.println(); // New line when complete
117+
System.out.flush();
118+
}
119+
};
120+
}
121+
122+
/**
123+
* Formats bytes into human-readable format.
124+
*
125+
* @param bytes The number of bytes to format
126+
* @return Formatted string (e.g., "1.5 MB", "512 KB")
127+
*/
128+
public static String formatBytes(long bytes) {
129+
if (bytes < 1024) return bytes + " B";
130+
if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0);
131+
if (bytes < 1024 * 1024 * 1024) return String.format("%.1f MB", bytes / (1024.0 * 1024.0));
132+
return String.format("%.1f GB", bytes / (1024.0 * 1024.0 * 1024.0));
133+
}
134+
}

src/main/java/io/github/lambdatest/gradle/TestSuiteUploader.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public class TestSuiteUploader {
1616
private String username;
1717
private String accessKey;
1818
private String testSuiteFilePath;
19+
private boolean showProgress;
1920

2021
/**
2122
* Creates a new TestSuiteUploader instance with the specified credentials and file path.
@@ -25,9 +26,24 @@ public class TestSuiteUploader {
2526
* @param testSuiteFilePath The path to the test suite file to be uploaded
2627
*/
2728
public TestSuiteUploader(String username, String accessKey, String testSuiteFilePath) {
29+
this(username, accessKey, testSuiteFilePath, false);
30+
}
31+
32+
/**
33+
* Creates a new TestSuiteUploader instance with the specified credentials, file path, and
34+
* progress tracking option.
35+
*
36+
* @param username The LambdaTest account username
37+
* @param accessKey The LambdaTest account access key
38+
* @param testSuiteFilePath The path to the test suite file to be uploaded
39+
* @param showProgress Whether to display upload progress in the console
40+
*/
41+
public TestSuiteUploader(
42+
String username, String accessKey, String testSuiteFilePath, boolean showProgress) {
2843
this.username = username;
2944
this.accessKey = accessKey;
3045
this.testSuiteFilePath = testSuiteFilePath;
46+
this.showProgress = showProgress;
3147
}
3248

3349
/**
@@ -42,7 +58,8 @@ public CompletableFuture<String> uploadTestSuiteAsync() {
4258
() -> {
4359
try {
4460
String testSuiteId =
45-
UploaderUtil.uploadAndGetId(username, accessKey, testSuiteFilePath);
61+
UploaderUtil.uploadAndGetId(
62+
username, accessKey, testSuiteFilePath, showProgress);
4663
logger.info("Uploaded test suite ID: {}", testSuiteId);
4764
return testSuiteId;
4865
} catch (IOException e) {

src/main/java/io/github/lambdatest/gradle/UploaderUtil.java

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,23 +40,50 @@ private UploaderUtil() {
4040
*/
4141
public static String uploadAndGetId(String username, String accessKey, String filePath)
4242
throws IOException {
43+
return uploadAndGetId(username, accessKey, filePath, false);
44+
}
45+
46+
/**
47+
* Uploads a file to LambdaTest and returns its ID with optional progress tracking.
48+
*
49+
* @implNote This method sends the file to {@link Constants#API_URL} and handles the multipart
50+
* form data construction and response parsing. When showProgress is true, it uses {@link
51+
* ProgressRequestBody} to track and display upload progress.
52+
* @param username The LambdaTest account username
53+
* @param accessKey The LambdaTest account access key
54+
* @param filePath The path to the file to be uploaded
55+
* @param showProgress Whether to display upload progress in the console
56+
* @return The ID of the uploaded file
57+
* @throws IOException if there's an error during file upload or response parsing
58+
*/
59+
public static String uploadAndGetId(
60+
String username, String accessKey, String filePath, boolean showProgress)
61+
throws IOException {
4362
OkHttpClient client =
4463
new OkHttpClient.Builder()
4564
.connectTimeout(1, TimeUnit.MINUTES) // Increase connection timeout
4665
.readTimeout(0, TimeUnit.MILLISECONDS) // Increase read timeout
4766
.writeTimeout(0, TimeUnit.MILLISECONDS) // Increase write timeout
4867
.build();
4968

69+
File file = new File(filePath);
5070
MediaType mediaType = MediaType.parse("application/octet-stream");
71+
RequestBody fileRequestBody = RequestBody.create(file, mediaType);
72+
5173
RequestBody body =
5274
new MultipartBody.Builder()
5375
.setType(MultipartBody.FORM)
54-
.addFormDataPart(
55-
"appFile",
56-
filePath,
57-
RequestBody.create(new File(filePath), mediaType))
76+
.addFormDataPart("appFile", filePath, fileRequestBody)
5877
.addFormDataPart("type", "espresso-android")
5978
.build();
79+
80+
// Wrap the entire multipart body with progress tracking if requested
81+
if (showProgress) {
82+
String fileName = file.getName();
83+
ProgressRequestBody.ProgressCallback callback =
84+
ProgressRequestBody.createConsoleCallback(fileName);
85+
body = new ProgressRequestBody(body, file, callback);
86+
}
6087
Request request =
6188
new Request.Builder()
6289
.url(Constants.API_URL)

0 commit comments

Comments
 (0)