Skip to content

Commit f11cce1

Browse files
Staacksclaude
andcommitted
Match the lab driver's contract for the BLE connect test.
Read tools/lab/ble.py and device.py properly this time. Two things the first version got wrong, both of which would have failed on the bench rather than here: The driver runs the test by name - "-e class de.rwth_aachen.phyphox.BleCompatConnectTest" - so the class is renamed to that. And AndroidDevice.prepare already sets debug.phyphox.remote, with cleanup clearing it, while the driver goes on talking to the phone after the test returns; setting and clearing it inside the test would have pulled the remote API out from under the host's assertions. So the connect test no longer touches the switch, and the seam check that does moves to its own class, where its teardown cannot race the driver. prepare grants the sensor permissions but not BLUETOOTH_CONNECT and BLUETOOTH_SCAN, and autoConfirm deliberately does not answer system permission dialogs - verified here that the flow stops on one. The test grants them itself so it stands alone either way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent e78e21b commit f11cce1

2 files changed

Lines changed: 116 additions & 59 deletions

File tree

app/src/androidTest/java/de/rwth_aachen/phyphox/BleCompatTest.java renamed to app/src/androidTest/java/de/rwth_aachen/phyphox/BleCompatConnectTest.java

Lines changed: 28 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import android.content.Context;
1010
import android.content.Intent;
11+
import android.os.Build;
1112

1213
import androidx.test.ext.junit.runners.AndroidJUnit4;
1314
import androidx.test.platform.app.InstrumentationRegistry;
@@ -31,24 +32,25 @@
3132
//drift; this keeps them in one file and still exercises the real scan UI, which is part of what
3233
//the suite protects.
3334
//
34-
//So this test ends where the host takes over: the experiment the device offers is loaded, not
35-
//started, and serving the remote API. It asserts nothing about the data.
35+
//So this test ends where the host takes over: the experiment the device offers is loaded and
36+
//NOT started. It asserts nothing about the data.
3637
//
37-
//The device name comes from the driver, which flashes the library examples UNMODIFIED and knows
38-
//what they advertise as:
38+
//The class name is the driver's, not a choice - tools/lab/ble.py runs exactly
3939
//
40-
// adb shell am instrument -e class de.rwth_aachen.phyphox.BleCompatTest \
41-
// -e bleDevice phyphox-arduino ...
40+
// am instrument -e class de.rwth_aachen.phyphox.BleCompatConnectTest \
41+
// -e bleDevice <name> ...
4242
//
43-
//Without that parameter there is no board to talk to and that test skips itself, which is what
44-
//happens in CI - the row needs hardware and runs in the lab. The seam check below needs none and
45-
//runs everywhere, because a suite that cannot reach the phone is worth catching before the board
46-
//is even involved.
43+
//and the driver flashes the library examples UNMODIFIED, so the name is whatever they advertise
44+
//as. Without that parameter there is no board to talk to and this skips itself, which is what
45+
//happens in CI - the row needs hardware and runs in the lab.
46+
//
47+
//Nothing here touches debug.phyphox.remote. The driver owns it (AndroidDevice.prepare sets it,
48+
//cleanup clears it) and goes on to talk to the phone after this test returns, so clearing it
49+
//here would pull the API out from under the host's assertions.
4750
@RunWith(AndroidJUnit4.class)
48-
public class BleCompatTest {
51+
public class BleCompatConnectTest {
4952

5053
private static final String PACKAGE = "de.rwth_aachen.phyphox";
51-
private static final int PORT = 8080;
5254

5355
private UiDevice device() {
5456
return UiDevice.getInstance(getInstrumentation());
@@ -61,56 +63,23 @@ private UiObject2 waitForId(String id, long timeout) {
6163
@Before
6264
public void quietFirstRunDialogs() {
6365
FixtureExperiment.suppressHints();
66+
//The scan asks for these at runtime and autoConfirm deliberately does not answer system
67+
//permission dialogs, so an ungranted phone stops on one with no board in sight. The
68+
//driver grants the sensor permissions it knows about but not these, and granting them
69+
//here keeps the test standalone either way.
70+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
71+
grant("android.permission.BLUETOOTH_CONNECT", "android.permission.BLUETOOTH_SCAN");
72+
grant("android.permission.ACCESS_FINE_LOCATION", "android.permission.ACCESS_COARSE_LOCATION");
6473
}
6574

66-
//The precondition the whole suite rests on, and the one half of it that needs no board: an
67-
//experiment with a Bluetooth block serves the remote API when the switch is set, so the host
68-
//can reach it. The switch is applied where every experiment finishes loading
69-
//(Experiment.onExperimentLoaded), which is the same place a transferred one arrives at, so
70-
//this covers the delivered case too as far as it can be covered without hardware.
71-
@Test
72-
public void aBluetoothExperimentServesTheRemoteApiWhenTheSwitchIsSet() throws Exception {
73-
shell("setprop debug.phyphox.remote 1");
74-
shell("setprop debug.phyphox.remotePort " + PORT);
75-
try {
76-
FixtureExperiment.launchAssetWithoutWaiting("bluetooth/Heart Rate.phyphox");
77-
//It stops at "please pick a device" without one, which is exactly the state the host
78-
//finds a transferred experiment in before it starts it - and the API has to answer
79-
//there, not only once something is connected.
80-
long deadline = System.currentTimeMillis() + 30000;
81-
boolean answered = false;
82-
while (!answered && System.currentTimeMillis() < deadline) {
83-
answered = remoteApiAnswers();
84-
if (!answered)
85-
Thread.sleep(500);
86-
}
87-
assertTrue("the remote API did not come up for a Bluetooth experiment although "
88-
+ "debug.phyphox.remote is set - the host cannot reach a device-delivered "
89-
+ "experiment either", answered);
90-
} finally {
91-
shell("setprop debug.phyphox.remote '\"\"'");
92-
shell("setprop debug.phyphox.remotePort '\"\"'");
93-
FixtureExperiment.close(FixtureExperiment.activity());
94-
}
95-
}
96-
97-
private void shell(String command) throws Exception {
98-
device().executeShellCommand(command);
99-
}
100-
101-
private boolean remoteApiAnswers() {
102-
try {
103-
java.net.HttpURLConnection connection = (java.net.HttpURLConnection)
104-
new java.net.URL("http://127.0.0.1:" + PORT + "/config").openConnection();
105-
connection.setConnectTimeout(2000);
106-
connection.setReadTimeout(2000);
107-
try (java.io.InputStream in = connection.getInputStream()) {
108-
return in.read() > 0;
109-
} finally {
110-
connection.disconnect();
75+
private void grant(String... permissions) {
76+
for (String permission : permissions) {
77+
try {
78+
getInstrumentation().getUiAutomation().grantRuntimePermission(PACKAGE, permission);
79+
} catch (Exception e) {
80+
//Not declared on this API level, or already granted - either way not this test's
81+
//problem; the scan below fails with a message if it actually mattered.
11182
}
112-
} catch (Exception e) {
113-
return false;
11483
}
11584
}
11685

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package de.rwth_aachen.phyphox;
2+
3+
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
4+
import static org.junit.Assert.assertTrue;
5+
6+
import androidx.test.ext.junit.runners.AndroidJUnit4;
7+
import androidx.test.uiautomator.UiDevice;
8+
9+
import org.junit.Before;
10+
import org.junit.Test;
11+
import org.junit.runner.RunWith;
12+
13+
//The precondition the BLE compatibility suite rests on, and the half of it that needs no board.
14+
//
15+
//That suite drives the phone from the host over the remote API (phyphox-docs/tools/lab/ble.py):
16+
//an instrumented test picks the device out of the scan, and everything after that is asserted
17+
//from outside. Which only works if an experiment that arrives from a Bluetooth device serves the
18+
//remote API when debug.phyphox.remote is set.
19+
//
20+
//It does, and not by a special case: the switch is applied in Experiment.onExperimentLoaded,
21+
//where EVERY experiment finishes loading, and every Bluetooth route ends up there through
22+
//ExperimentListActivity - BluetoothExperimentLoader's success callback and the zip handler both
23+
//just start the Experiment activity. So a delivered experiment gets it exactly as a launched one
24+
//does, and there was nothing to extend.
25+
//
26+
//But that is a seam nobody would think to preserve while refactoring, and if it breaks the whole
27+
//lab suite reports a phone it cannot reach rather than a bug it found. Hence this test: it is
28+
//cheap, it needs no hardware, and it fails with a message naming that consequence.
29+
//
30+
//Separate class from BleCompatConnectTest on purpose. That class is what the driver runs by name
31+
//while it owns the switch itself, and this one sets and clears the switch around its own run -
32+
//in the same class the clearing would race the driver's assertions.
33+
@RunWith(AndroidJUnit4.class)
34+
public class BluetoothRemoteSeamTest {
35+
36+
private static final int PORT = 8080;
37+
38+
@Before
39+
public void quietFirstRunDialogs() {
40+
FixtureExperiment.suppressHints();
41+
}
42+
43+
@Test
44+
public void aBluetoothExperimentServesTheRemoteApiWhenTheSwitchIsSet() throws Exception {
45+
shell("setprop debug.phyphox.remote 1");
46+
shell("setprop debug.phyphox.remotePort " + PORT);
47+
try {
48+
FixtureExperiment.launchAssetWithoutWaiting("bluetooth/Heart Rate.phyphox");
49+
//It stops at "please pick a device" without one, which is exactly the state the host
50+
//finds a transferred experiment in before it starts it - and the API has to answer
51+
//there, not only once something is connected.
52+
long deadline = System.currentTimeMillis() + 30000;
53+
boolean answered = false;
54+
while (!answered && System.currentTimeMillis() < deadline) {
55+
answered = remoteApiAnswers();
56+
if (!answered)
57+
Thread.sleep(500);
58+
}
59+
assertTrue("the remote API did not come up for a Bluetooth experiment although "
60+
+ "debug.phyphox.remote is set - the host cannot reach a device-delivered "
61+
+ "experiment either", answered);
62+
} finally {
63+
shell("setprop debug.phyphox.remote '\"\"'");
64+
shell("setprop debug.phyphox.remotePort '\"\"'");
65+
FixtureExperiment.close(FixtureExperiment.activity());
66+
}
67+
}
68+
69+
private void shell(String command) throws Exception {
70+
UiDevice.getInstance(getInstrumentation()).executeShellCommand(command);
71+
}
72+
73+
private boolean remoteApiAnswers() {
74+
try {
75+
java.net.HttpURLConnection connection = (java.net.HttpURLConnection)
76+
new java.net.URL("http://127.0.0.1:" + PORT + "/config").openConnection();
77+
connection.setConnectTimeout(2000);
78+
connection.setReadTimeout(2000);
79+
try (java.io.InputStream in = connection.getInputStream()) {
80+
return in.read() > 0;
81+
} finally {
82+
connection.disconnect();
83+
}
84+
} catch (Exception e) {
85+
return false;
86+
}
87+
}
88+
}

0 commit comments

Comments
 (0)