Skip to content

Commit 321e816

Browse files
Merge pull request #2322 from bugsnag/tms/idem-commands
Use Maze Runner dedicated idempotent commands
2 parents ad780f5 + 1c7ec2d commit 321e816

File tree

4 files changed

+95
-67
lines changed

4 files changed

+95
-67
lines changed

features/fixtures/mazerunner/app/src/main/AndroidManifest.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
android:name=".MazerunnerApp"
1111
android:extractNativeLibs="false"
1212
>
13+
<!-- configChanges is set to prevent activity recreation on orientation change,
14+
as it leads to an extra thread polling for commands. -->
1315
<activity
1416
android:name=".MainActivity"
17+
android:configChanges="orientation"
1518
android:exported="true"
1619
>
1720
<intent-filter>

features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/MainActivity.kt

Lines changed: 39 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import android.os.Looper
1111
import android.view.Window
1212
import android.widget.Button
1313
import android.widget.EditText
14-
import com.bugsnag.android.BugsnagInternals
1514
import com.bugsnag.android.mazerunner.scenarios.Scenario
1615
import org.json.JSONObject
1716
import java.io.File
@@ -21,21 +20,23 @@ import java.net.URL
2120
import kotlin.concurrent.thread
2221
import kotlin.math.max
2322

24-
const val CONFIG_FILE_TIMEOUT = 5000
23+
const val CONFIG_FILE_TIMEOUT = 15000
2524

2625
class MainActivity : Activity() {
2726

2827
private val mainHandler = Handler(Looper.getMainLooper())
2928

3029
private val apiKeyKey = "BUGSNAG_API_KEY"
30+
private val commandUUIDKey = "MAZE_COMMAND_UUID"
3131
lateinit var prefs: SharedPreferences
3232

3333
var scenario: Scenario? = null
34-
var polling = false
34+
var isActivityRecreate = false
3535
var mazeAddress: String? = null
3636

3737
override fun onCreate(savedInstanceState: Bundle?) {
3838
super.onCreate(savedInstanceState)
39+
this.isActivityRecreate = savedInstanceState != null
3940
log("MainActivity.onCreate called")
4041
requestWindowFeature(Window.FEATURE_NO_TITLE)
4142
setContentView(R.layout.activity_main)
@@ -71,7 +72,9 @@ class MainActivity : Activity() {
7172
super.onResume()
7273
log("MainActivity.onResume called")
7374

74-
if (!polling) {
75+
// Don't start the command runner again if the activity is being recreated,
76+
// as it results in two threads executing commands concurrently and causing flakes.
77+
if (!this.isActivityRecreate) {
7578
startCommandRunner()
7679
}
7780
log("MainActivity.onResume complete")
@@ -122,14 +125,33 @@ class MainActivity : Activity() {
122125
return jsonObject?.optString(key) ?: ""
123126
}
124127

128+
private fun setStoredCommandUUID(commandUUID: String) {
129+
with(prefs.edit()) {
130+
putString(commandUUIDKey, commandUUID)
131+
commit()
132+
}
133+
CiLog.info("lastCommandUUID set to: $commandUUID")
134+
}
135+
136+
private fun clearStoredCommandUUID() {
137+
with(prefs.edit()) {
138+
remove(commandUUIDKey)
139+
commit()
140+
}
141+
CiLog.info("lastCommandUUID set to empty")
142+
}
143+
144+
private fun getStoredCommandUUID(): String? {
145+
return prefs.getString(commandUUIDKey, "").orEmpty()
146+
}
147+
125148
// Starts a thread to poll for Maze Runner actions to perform
126149
private fun startCommandRunner() {
127-
// Get the next maze runner command
128-
polling = true
129150
thread(start = true) {
130151
if (mazeAddress == null) setMazeRunnerAddress()
131152
checkNetwork()
132153

154+
var polling = true
133155
while (polling) {
134156
Thread.sleep(1000)
135157
try {
@@ -142,17 +164,13 @@ class MainActivity : Activity() {
142164

143165
// Log the received command
144166
CiLog.info("Received command: $commandStr")
145-
var command = JSONObject(commandStr)
167+
val command = JSONObject(commandStr)
146168
val action = getStringSafely(command, "action")
147169
val scenarioName = getStringSafely(command, "scenario_name")
148170
val scenarioMode = getStringSafely(command, "scenario_mode")
149171
val sessionsUrl = getStringSafely(command, "sessions_endpoint")
150172
val notifyUrl = getStringSafely(command, "notify_endpoint")
151-
log("command.action: $action")
152-
log("command.scenarioName: $scenarioName")
153-
log("command.scenarioMode: $scenarioMode")
154-
log("command.sessionsUrl: $sessionsUrl")
155-
log("command.notifyUrl: $notifyUrl")
173+
val commandUUID = getStringSafely(command, "uuid")
156174

157175
// Stop polling once we have a scenario action
158176
if ("start_bugsnag".equals(action) || "run_scenario".equals(action)) {
@@ -172,13 +190,18 @@ class MainActivity : Activity() {
172190
CiLog.info("No Maze Runner command queuing, continuing to poll")
173191
}
174192
"start_bugsnag" -> {
193+
setStoredCommandUUID(commandUUID)
175194
startBugsnag(scenarioName, scenarioMode, sessionsUrl, notifyUrl)
176195
}
177196
"run_scenario" -> {
197+
setStoredCommandUUID(commandUUID)
178198
runScenario(scenarioName, scenarioMode, sessionsUrl, notifyUrl)
179199
}
180-
"clear_persistent_data" -> clearPersistentData()
181-
"flush" -> BugsnagInternals.flush()
200+
"clear_persistent_data" -> {
201+
setStoredCommandUUID(commandUUID)
202+
PersistentData(applicationContext).clear()
203+
}
204+
"reset_uuid" -> clearStoredCommandUUID()
182205
else -> throw IllegalArgumentException("Unknown action: $action")
183206
}
184207
}
@@ -190,7 +213,8 @@ class MainActivity : Activity() {
190213
}
191214

192215
private fun readCommand(): String {
193-
val commandUrl = "http://$mazeAddress/command"
216+
val commandUrl = "http://$mazeAddress/idem-command?after=${getStoredCommandUUID()}"
217+
CiLog.info("Requesting Maze Runner command from: $commandUrl")
194218
val urlConnection = URL(commandUrl).openConnection() as HttpURLConnection
195219
try {
196220
return urlConnection.inputStream.use { it.reader().readText() }
@@ -246,52 +270,6 @@ class MainActivity : Activity() {
246270
}
247271
}
248272

249-
// Clear persistent data (used to stop scenarios bleeding into each other)
250-
private fun clearPersistentData() {
251-
CiLog.info("Clearing persistent data")
252-
clearCacheFolder("bugsnag")
253-
clearCacheFolder("StrictModeDiscScenarioFile")
254-
clearFilesFolder("background-service-dir")
255-
256-
removeFile("device-id")
257-
removeFile("internal-device-id")
258-
259-
listFolders()
260-
}
261-
262-
// Recursively deletes the contents of a folder beneath /cache
263-
private fun clearCacheFolder(name: String) {
264-
val folder = File(applicationContext.cacheDir, name)
265-
log("Clearing folder: ${folder.path}")
266-
folder.deleteRecursively()
267-
}
268-
269-
private fun clearFilesFolder(name: String) {
270-
val folder = File(applicationContext.filesDir, name)
271-
log("Clearing folder: ${folder.path}")
272-
folder.deleteRecursively()
273-
}
274-
275-
// Deletes a file beneath /files
276-
private fun removeFile(name: String) {
277-
val file = File(applicationContext.filesDir, name)
278-
log("Removing file: ${file.path}")
279-
file.delete()
280-
}
281-
282-
// Logs out the contents of the /cache and /files folders
283-
private fun listFolders() {
284-
log("Contents of: ${applicationContext.cacheDir}")
285-
applicationContext.cacheDir.walkTopDown().forEach {
286-
log(it.absolutePath)
287-
}
288-
289-
log("Contents of: ${applicationContext.filesDir}")
290-
applicationContext.filesDir.walkTopDown().forEach {
291-
log(it.absolutePath)
292-
}
293-
}
294-
295273
private fun loadScenario(
296274
eventType: String,
297275
mode: String,
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.bugsnag.android.mazerunner
2+
3+
import android.content.Context
4+
import java.io.File
5+
6+
class PersistentData(private val applicationContext: Context) {
7+
8+
// Clear persistent data (used to stop scenarios bleeding into each other)
9+
fun clear() {
10+
CiLog.info("Clearing persistent data")
11+
clearCacheFolder("bugsnag")
12+
clearCacheFolder("StrictModeDiscScenarioFile")
13+
clearFilesFolder("background-service-dir")
14+
15+
removeFile("device-id")
16+
removeFile("internal-device-id")
17+
18+
listFolders()
19+
}
20+
21+
// Recursively deletes the contents of a folder beneath /cache
22+
private fun clearCacheFolder(name: String) {
23+
val folder = File(applicationContext.cacheDir, name)
24+
log("Clearing folder: ${folder.path}")
25+
folder.deleteRecursively()
26+
}
27+
28+
private fun clearFilesFolder(name: String) {
29+
val folder = File(applicationContext.filesDir, name)
30+
log("Clearing folder: ${folder.path}")
31+
folder.deleteRecursively()
32+
}
33+
34+
// Deletes a file beneath /files
35+
private fun removeFile(name: String) {
36+
val file = File(applicationContext.filesDir, name)
37+
log("Removing file: ${file.path}")
38+
file.delete()
39+
}
40+
41+
// Logs out the contents of the /cache and /files folders
42+
private fun listFolders() {
43+
log("Contents of: ${applicationContext.cacheDir}")
44+
applicationContext.cacheDir.walkTopDown().forEach {
45+
log(it.absolutePath)
46+
}
47+
48+
log("Contents of: ${applicationContext.filesDir}")
49+
applicationContext.filesDir.walkTopDown().forEach {
50+
log(it.absolutePath)
51+
}
52+
}
53+
}

features/steps/android_steps.rb

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,6 @@ def execute_command(action, scenario_name = '')
2626
$sessions_endpoint = 'http://bs-local.com:9339/sessions'
2727
$notify_endpoint = 'http://bs-local.com:9339/notify'
2828
end
29-
30-
# Ensure fixture has read the command
31-
count = 600
32-
sleep 0.1 until Maze::Server.commands.size_remaining == 0 || (count -= 1) < 1
33-
34-
raise 'Test fixture did not GET /command' unless Maze::Server.commands.size_remaining == 0
3529
end
3630

3731
def press_at(x, y)

0 commit comments

Comments
 (0)