1 // Copyright 2015 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.net; 6 7 import static com.google.common.truth.Truth.assertThat; 8 import static com.google.common.truth.Truth.assertWithMessage; 9 10 import static org.chromium.net.CronetTestRule.getTestStorage; 11 import static org.chromium.net.truth.UrlResponseInfoSubject.assertThat; 12 13 import androidx.test.ext.junit.runners.AndroidJUnit4; 14 import androidx.test.filters.LargeTest; 15 import androidx.test.filters.SmallTest; 16 import com.android.testutils.SkipPresubmit; 17 18 import org.json.JSONObject; 19 import org.junit.After; 20 import org.junit.Before; 21 import org.junit.Rule; 22 import org.junit.Test; 23 import org.junit.runner.RunWith; 24 25 import org.chromium.base.test.util.DoNotBatch; 26 import org.chromium.net.CronetTestRule.CronetImplementation; 27 import org.chromium.net.CronetTestRule.IgnoreFor; 28 29 import java.io.File; 30 import java.io.FileInputStream; 31 import java.io.IOException; 32 import java.util.Date; 33 import java.util.concurrent.Executors; 34 35 /** Tests making requests using QUIC. */ 36 @DoNotBatch(reason = "crbug/1459563") 37 @RunWith(AndroidJUnit4.class) 38 @IgnoreFor( 39 implementations = {CronetImplementation.FALLBACK, CronetImplementation.AOSP_PLATFORM}, 40 reason = 41 "The fallback implementation doesn't support QUIC. " 42 + "crbug.com/1494870: Enable for AOSP_PLATFORM once fixed") 43 public class QuicTest { 44 @Rule public final CronetTestRule mTestRule = CronetTestRule.withManualEngineStartup(); 45 46 @Before setUp()47 public void setUp() throws Exception { 48 // Load library first, since we need the Quic test server's URL. 49 System.loadLibrary("cronet_tests"); 50 QuicTestServer.startQuicTestServer(mTestRule.getTestFramework().getContext()); 51 52 mTestRule 53 .getTestFramework() 54 .applyEngineBuilderPatch( 55 (builder) -> { 56 builder.enableNetworkQualityEstimator(true).enableQuic(true); 57 builder.addQuicHint( 58 QuicTestServer.getServerHost(), 59 QuicTestServer.getServerPort(), 60 QuicTestServer.getServerPort()); 61 62 // The pref may not be written if the computed Effective Connection Type 63 // (ECT) matches the default ECT for the current connection type. 64 // Force the ECT to "Slow-2G". Since "Slow-2G" is not the default ECT 65 // for any connection type, this ensures that the pref is written to. 66 JSONObject nqeParams = 67 new JSONObject() 68 .put("force_effective_connection_type", "Slow-2G"); 69 70 // TODO(mgersh): Enable connection migration once it works, see 71 // http://crbug.com/634910 72 JSONObject quicParams = 73 new JSONObject() 74 .put("connection_options", "PACE,IW10,FOO,DEADBEEF") 75 .put("max_server_configs_stored_in_properties", 2) 76 .put("idle_connection_timeout_seconds", 300) 77 .put("migrate_sessions_on_network_change_v2", false) 78 .put("migrate_sessions_early_v2", false) 79 .put("race_cert_verification", true); 80 JSONObject hostResolverParams = 81 CronetTestUtil.generateHostResolverRules(); 82 JSONObject experimentalOptions = 83 new JSONObject() 84 .put("QUIC", quicParams) 85 .put("HostResolverRules", hostResolverParams) 86 .put("NetworkQualityEstimator", nqeParams); 87 builder.setExperimentalOptions(experimentalOptions.toString()); 88 builder.setStoragePath( 89 getTestStorage(mTestRule.getTestFramework().getContext())); 90 builder.enableHttpCache( 91 CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 1000 * 1024); 92 CronetTestUtil.setMockCertVerifierForTesting( 93 builder, QuicTestServer.createMockCertVerifier()); 94 }); 95 mTestRule.getTestFramework().startEngine(); 96 } 97 98 @After tearDown()99 public void tearDown() throws Exception { 100 QuicTestServer.shutdownQuicTestServer(); 101 } 102 103 @Test 104 @LargeTest testQuicLoadUrl()105 public void testQuicLoadUrl() throws Exception { 106 ExperimentalCronetEngine cronetEngine = mTestRule.getTestFramework().getEngine(); 107 String quicURL = QuicTestServer.getServerURL() + "/simple.txt"; 108 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 109 110 // Although the native stack races QUIC and SPDY for the first request, 111 // since there is no http server running on the corresponding TCP port, 112 // QUIC will always succeed with a 200 (see 113 // net::HttpStreamFactoryImpl::Request::OnStreamFailed). 114 UrlRequest.Builder requestBuilder = 115 cronetEngine.newUrlRequestBuilder(quicURL, callback, callback.getExecutor()); 116 requestBuilder.build().start(); 117 callback.blockForDone(); 118 119 assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200); 120 String expectedContent = "This is a simple text file served by QUIC.\n"; 121 assertThat(callback.mResponseAsString).isEqualTo(expectedContent); 122 assertIsQuic(callback.getResponseInfoWithChecks()); 123 // The total received bytes should be larger than the content length, to account for 124 // headers. 125 assertThat(callback.getResponseInfoWithChecks()) 126 .hasReceivedByteCountThat() 127 .isGreaterThan((long) expectedContent.length()); 128 CronetTestUtil.nativeFlushWritePropertiesForTesting(cronetEngine); 129 assertThat( 130 fileContainsString( 131 "local_prefs.json", 132 QuicTestServer.getServerHost() 133 + ":" 134 + QuicTestServer.getServerPort())) 135 .isTrue(); 136 cronetEngine.shutdown(); 137 138 // Make another request using a new context but with no QUIC hints. 139 ExperimentalCronetEngine.Builder builder = 140 new ExperimentalCronetEngine.Builder(mTestRule.getTestFramework().getContext()); 141 builder.setStoragePath(getTestStorage(mTestRule.getTestFramework().getContext())); 142 builder.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK, 1000 * 1024); 143 builder.enableQuic(true); 144 JSONObject hostResolverParams = CronetTestUtil.generateHostResolverRules(); 145 JSONObject experimentalOptions = 146 new JSONObject().put("HostResolverRules", hostResolverParams); 147 builder.setExperimentalOptions(experimentalOptions.toString()); 148 CronetTestUtil.setMockCertVerifierForTesting( 149 builder, QuicTestServer.createMockCertVerifier()); 150 cronetEngine = builder.build(); 151 TestUrlRequestCallback callback2 = new TestUrlRequestCallback(); 152 requestBuilder = 153 cronetEngine.newUrlRequestBuilder(quicURL, callback2, callback2.getExecutor()); 154 requestBuilder.build().start(); 155 callback2.blockForDone(); 156 assertThat(callback2.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200); 157 assertThat(callback2.mResponseAsString).isEqualTo(expectedContent); 158 assertIsQuic(callback.getResponseInfoWithChecks()); 159 // The total received bytes should be larger than the content length, to account for 160 // headers. 161 assertThat(callback2.getResponseInfoWithChecks()) 162 .hasReceivedByteCountThat() 163 .isGreaterThan((long) expectedContent.length()); 164 cronetEngine.shutdown(); 165 } 166 167 // Returns whether a file contains a particular string. fileContainsString(String filename, String content)168 private boolean fileContainsString(String filename, String content) throws IOException { 169 File file = 170 new File( 171 getTestStorage(mTestRule.getTestFramework().getContext()) 172 + "/prefs/" 173 + filename); 174 FileInputStream fileInputStream = new FileInputStream(file); 175 byte[] data = new byte[(int) file.length()]; 176 fileInputStream.read(data); 177 fileInputStream.close(); 178 return new String(data, "UTF-8").contains(content); 179 } 180 181 /** Tests that the network quality listeners are propoerly notified when QUIC is enabled. */ 182 @Test 183 @LargeTest 184 @SuppressWarnings("deprecation") testNQEWithQuic()185 public void testNQEWithQuic() throws Exception { 186 ExperimentalCronetEngine cronetEngine = mTestRule.getTestFramework().getEngine(); 187 String quicURL = QuicTestServer.getServerURL() + "/simple.txt"; 188 189 TestNetworkQualityRttListener rttListener = 190 new TestNetworkQualityRttListener(Executors.newSingleThreadExecutor()); 191 TestNetworkQualityThroughputListener throughputListener = 192 new TestNetworkQualityThroughputListener(Executors.newSingleThreadExecutor()); 193 194 cronetEngine.addRttListener(rttListener); 195 cronetEngine.addThroughputListener(throughputListener); 196 197 cronetEngine.configureNetworkQualityEstimatorForTesting(true, true, true); 198 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 199 200 // Although the native stack races QUIC and SPDY for the first request, 201 // since there is no http server running on the corresponding TCP port, 202 // QUIC will always succeed with a 200 (see 203 // net::HttpStreamFactoryImpl::Request::OnStreamFailed). 204 UrlRequest.Builder requestBuilder = 205 cronetEngine.newUrlRequestBuilder(quicURL, callback, callback.getExecutor()); 206 requestBuilder.build().start(); 207 callback.blockForDone(); 208 209 assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200); 210 String expectedContent = "This is a simple text file served by QUIC.\n"; 211 assertThat(callback.mResponseAsString).isEqualTo(expectedContent); 212 assertIsQuic(callback.getResponseInfoWithChecks()); 213 214 // Throughput observation is posted to the network quality estimator on the network thread 215 // after the UrlRequest is completed. The observations are then eventually posted to 216 // throughput listeners on the executor provided to network quality. 217 throughputListener.waitUntilFirstThroughputObservationReceived(); 218 219 // Wait for RTT observation (at the URL request layer) to be posted. 220 rttListener.waitUntilFirstUrlRequestRTTReceived(); 221 222 assertThat(throughputListener.throughputObservationCount()).isGreaterThan(0); 223 224 // Check RTT observation count after throughput observation has been received. This ensures 225 // that executor has finished posting the RTT observation to the RTT listeners. 226 // NETWORK_QUALITY_OBSERVATION_SOURCE_URL_REQUEST 227 assertThat(rttListener.rttObservationCount(0)).isGreaterThan(0); 228 229 // NETWORK_QUALITY_OBSERVATION_SOURCE_QUIC 230 assertThat(rttListener.rttObservationCount(2)).isGreaterThan(0); 231 232 // Verify that effective connection type callback is received and 233 // effective connection type is correctly set. 234 assertThat(cronetEngine.getEffectiveConnectionType()) 235 .isNotEqualTo(EffectiveConnectionType.TYPE_UNKNOWN); 236 237 // Verify that the HTTP RTT, transport RTT and downstream throughput 238 // estimates are available. 239 assertThat(cronetEngine.getHttpRttMs()).isAtLeast(0); 240 assertThat(cronetEngine.getTransportRttMs()).isAtLeast(0); 241 assertThat(cronetEngine.getDownstreamThroughputKbps()).isAtLeast(0); 242 243 CronetTestUtil.nativeFlushWritePropertiesForTesting(cronetEngine); 244 assertThat(fileContainsString("local_prefs.json", "network_qualities")).isTrue(); 245 cronetEngine.shutdown(); 246 } 247 248 @Test 249 @SmallTest 250 @SkipPresubmit(reason = "b/293141085 Tests that enable disk cache are flaky") testMetricsWithQuic()251 public void testMetricsWithQuic() throws Exception { 252 ExperimentalCronetEngine cronetEngine = mTestRule.getTestFramework().getEngine(); 253 TestRequestFinishedListener requestFinishedListener = new TestRequestFinishedListener(); 254 cronetEngine.addRequestFinishedListener(requestFinishedListener); 255 256 String quicURL = QuicTestServer.getServerURL() + "/simple.txt"; 257 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 258 259 UrlRequest.Builder requestBuilder = 260 cronetEngine.newUrlRequestBuilder(quicURL, callback, callback.getExecutor()); 261 Date startTime = new Date(); 262 requestBuilder.build().start(); 263 callback.blockForDone(); 264 requestFinishedListener.blockUntilDone(); 265 Date endTime = new Date(); 266 267 assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200); 268 assertIsQuic(callback.getResponseInfoWithChecks()); 269 270 RequestFinishedInfo requestInfo = requestFinishedListener.getRequestInfo(); 271 MetricsTestUtil.checkRequestFinishedInfo(requestInfo, quicURL, startTime, endTime); 272 assertThat(requestInfo.getFinishedReason()).isEqualTo(RequestFinishedInfo.SUCCEEDED); 273 MetricsTestUtil.checkHasConnectTiming(requestInfo.getMetrics(), startTime, endTime, true); 274 275 // Second request should use the same connection and not have ConnectTiming numbers 276 callback = new TestUrlRequestCallback(); 277 requestFinishedListener.reset(); 278 requestBuilder = 279 cronetEngine.newUrlRequestBuilder(quicURL, callback, callback.getExecutor()); 280 startTime = new Date(); 281 requestBuilder.build().start(); 282 callback.blockForDone(); 283 requestFinishedListener.blockUntilDone(); 284 endTime = new Date(); 285 286 assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200); 287 assertIsQuic(callback.getResponseInfoWithChecks()); 288 289 requestInfo = requestFinishedListener.getRequestInfo(); 290 MetricsTestUtil.checkRequestFinishedInfo(requestInfo, quicURL, startTime, endTime); 291 assertThat(requestInfo.getFinishedReason()).isEqualTo(RequestFinishedInfo.SUCCEEDED); 292 MetricsTestUtil.checkNoConnectTiming(requestInfo.getMetrics()); 293 294 cronetEngine.shutdown(); 295 } 296 297 // Helper method to assert that the request is negotiated over QUIC. assertIsQuic(UrlResponseInfo responseInfo)298 private void assertIsQuic(UrlResponseInfo responseInfo) { 299 String protocol = responseInfo.getNegotiatedProtocol(); 300 assertWithMessage("Expected the negotiatedProtocol to be QUIC but was " + protocol) 301 .that(protocol.startsWith("http/2+quic") || protocol.startsWith("h3")) 302 .isTrue(); 303 } 304 } 305