1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.health.connect; 18 19 import static org.hamcrest.CoreMatchers.containsString; 20 21 import android.health.connect.ratelimiter.RateLimiter; 22 import android.health.connect.ratelimiter.RateLimiter.QuotaCategory; 23 24 import androidx.test.ext.junit.runners.AndroidJUnit4; 25 26 import org.junit.Rule; 27 import org.junit.Test; 28 import org.junit.rules.ExpectedException; 29 import org.junit.runner.RunWith; 30 31 import java.time.Duration; 32 import java.time.Instant; 33 34 @RunWith(AndroidJUnit4.class) 35 public class RateLimiterTest { 36 private static final int UID = 1; 37 private static final boolean IS_IN_FOREGROUND_TRUE = true; 38 private static final boolean IS_IN_FOREGROUND_FALSE = false; 39 private static final int MAX_FOREGROUND_READ_CALL_15M = 2000; 40 private static final int MAX_BACKGROUND_CALL_15M = 1000; 41 private static final Duration WINDOW_15M = Duration.ofMinutes(15); 42 private static final int MEMORY_COST = 20000; 43 44 @Rule public ExpectedException exception = ExpectedException.none(); 45 46 private final RateLimiter mRateLimiter = new RateLimiter(); 47 48 @Test testTryAcquireApiCallQuota_invalidQuotaCategory()49 public void testTryAcquireApiCallQuota_invalidQuotaCategory() { 50 mRateLimiter.clearCache(); 51 @QuotaCategory.Type int quotaCategory = 0; 52 exception.expect(IllegalArgumentException.class); 53 exception.expectMessage("Quota category not defined."); 54 mRateLimiter.tryAcquireApiCallQuota(UID, quotaCategory, IS_IN_FOREGROUND_TRUE); 55 } 56 57 @Test testTryAcquireApiCallQuota_unmeteredForegroundCalls()58 public void testTryAcquireApiCallQuota_unmeteredForegroundCalls() { 59 mRateLimiter.clearCache(); 60 @QuotaCategory.Type int quotaCategory = 1; 61 tryAcquireCallQuotaNTimes( 62 quotaCategory, IS_IN_FOREGROUND_TRUE, MAX_FOREGROUND_READ_CALL_15M + 1); 63 } 64 65 @Test testTryAcquireApiCallQuota_unmeteredBackgroundCalls()66 public void testTryAcquireApiCallQuota_unmeteredBackgroundCalls() { 67 mRateLimiter.clearCache(); 68 @QuotaCategory.Type int quotaCategory = 1; 69 tryAcquireCallQuotaNTimes( 70 quotaCategory, IS_IN_FOREGROUND_TRUE, MAX_BACKGROUND_CALL_15M + 1); 71 } 72 73 @Test testTryAcquireApiCallQuota_meteredForegroundCallsInLimit()74 public void testTryAcquireApiCallQuota_meteredForegroundCallsInLimit() { 75 mRateLimiter.clearCache(); 76 @QuotaCategory.Type int quotaCategoryRead = 2; 77 tryAcquireCallQuotaNTimes( 78 quotaCategoryRead, IS_IN_FOREGROUND_TRUE, MAX_FOREGROUND_READ_CALL_15M); 79 } 80 81 @Test testTryAcquireApiCallQuota_meteredBackgroundCallsInLimit()82 public void testTryAcquireApiCallQuota_meteredBackgroundCallsInLimit() { 83 mRateLimiter.clearCache(); 84 @QuotaCategory.Type int quotaCategoryWrite = 3; 85 tryAcquireCallQuotaNTimes( 86 quotaCategoryWrite, IS_IN_FOREGROUND_FALSE, MAX_BACKGROUND_CALL_15M); 87 } 88 89 @Test testTryAcquireApiCallQuota_meteredForegroundCallsLimitExceeded()90 public void testTryAcquireApiCallQuota_meteredForegroundCallsLimitExceeded() { 91 mRateLimiter.clearCache(); 92 @QuotaCategory.Type int quotaCategoryRead = 2; 93 Instant startTime = Instant.now(); 94 tryAcquireCallQuotaNTimes( 95 quotaCategoryRead, IS_IN_FOREGROUND_TRUE, MAX_FOREGROUND_READ_CALL_15M); 96 Instant endTime = Instant.now(); 97 int ceilQuotaAcquired = 98 getCeilQuotaAcquired(startTime, endTime, WINDOW_15M, MAX_FOREGROUND_READ_CALL_15M); 99 exception.expect(HealthConnectException.class); 100 exception.expectMessage(containsString("API call quota exceeded")); 101 tryAcquireCallQuotaNTimes(quotaCategoryRead, IS_IN_FOREGROUND_TRUE, ceilQuotaAcquired); 102 } 103 104 @Test testTryAcquireApiCallQuota_meteredBackgroundCallsLimitExceeded()105 public void testTryAcquireApiCallQuota_meteredBackgroundCallsLimitExceeded() { 106 mRateLimiter.clearCache(); 107 @QuotaCategory.Type int quotaCategoryWrite = 3; 108 Instant startTime = Instant.now(); 109 tryAcquireCallQuotaNTimes( 110 quotaCategoryWrite, IS_IN_FOREGROUND_FALSE, MAX_BACKGROUND_CALL_15M); 111 Instant endTime = Instant.now(); 112 int ceilQuotaAcquired = 113 getCeilQuotaAcquired(startTime, endTime, WINDOW_15M, MAX_BACKGROUND_CALL_15M); 114 exception.expect(HealthConnectException.class); 115 exception.expectMessage(containsString("API call quota exceeded")); 116 tryAcquireCallQuotaNTimes(quotaCategoryWrite, IS_IN_FOREGROUND_FALSE, ceilQuotaAcquired); 117 } 118 119 @Test testRecordMemoryRollingQuota_exceedBackgroundLimit()120 public void testRecordMemoryRollingQuota_exceedBackgroundLimit() throws InterruptedException { 121 mRateLimiter.clearCache(); 122 @QuotaCategory.Type int quotaCategoryWrite = 3; 123 exception.expect(HealthConnectException.class); 124 exception.expectMessage(containsString("API call quota exceeded")); 125 tryAcquireCallQuotaNTimes( 126 quotaCategoryWrite, IS_IN_FOREGROUND_FALSE, MAX_BACKGROUND_CALL_15M, 40000); 127 } 128 129 @Test checkMaxChunkMemoryUsage_LimitExceeded()130 public void checkMaxChunkMemoryUsage_LimitExceeded() { 131 long valueExceeding = 5000001; 132 exception.expect(HealthConnectException.class); 133 exception.expectMessage( 134 "Records chunk size exceeded the max chunk limit: 5000000, was: 5000001"); 135 mRateLimiter.checkMaxChunkMemoryUsage(valueExceeding); 136 } 137 138 @Test checkMaxChunkMemoryUsage_inLimit()139 public void checkMaxChunkMemoryUsage_inLimit() { 140 long value = 5000000; 141 mRateLimiter.checkMaxChunkMemoryUsage(value); 142 } 143 144 @Test checkMaxRecordMemoryUsage_LimitExceeded()145 public void checkMaxRecordMemoryUsage_LimitExceeded() { 146 long valueExceeding = 1000001; 147 exception.expect(HealthConnectException.class); 148 exception.expectMessage( 149 "Record size exceeded the single record size limit: 1000000, was: 1000001"); 150 mRateLimiter.checkMaxRecordMemoryUsage(valueExceeding); 151 } 152 153 @Test checkMaxRecordMemoryUsage_inLimit()154 public void checkMaxRecordMemoryUsage_inLimit() { 155 long value = 1000000; 156 mRateLimiter.checkMaxRecordMemoryUsage(value); 157 } 158 getCeilQuotaAcquired( Instant startTime, Instant endTime, Duration window, int maxQuota)159 private int getCeilQuotaAcquired( 160 Instant startTime, Instant endTime, Duration window, int maxQuota) { 161 Duration timeSpent = Duration.between(startTime, endTime); 162 float accumulated = timeSpent.toMillis() * ((float) maxQuota / (float) window.toMillis()); 163 return accumulated > (int) accumulated 164 ? (int) Math.ceil(accumulated) 165 : (int) accumulated + 1; 166 } 167 tryAcquireCallQuotaNTimes( @uotaCategory.Type int quotaCategory, boolean isInForeground, int nTimes)168 private void tryAcquireCallQuotaNTimes( 169 @QuotaCategory.Type int quotaCategory, boolean isInForeground, int nTimes) { 170 171 if (quotaCategory == QuotaCategory.QUOTA_CATEGORY_WRITE) { 172 for (int i = 0; i < nTimes; i++) { 173 mRateLimiter.tryAcquireApiCallQuota( 174 UID, quotaCategory, isInForeground, MEMORY_COST); 175 } 176 } else { 177 for (int i = 0; i < nTimes; i++) { 178 mRateLimiter.tryAcquireApiCallQuota(UID, quotaCategory, isInForeground); 179 } 180 } 181 } 182 tryAcquireCallQuotaNTimes( @uotaCategory.Type int quotaCategory, boolean isInForeground, int nTimes, int memoryCost)183 private void tryAcquireCallQuotaNTimes( 184 @QuotaCategory.Type int quotaCategory, 185 boolean isInForeground, 186 int nTimes, 187 int memoryCost) { 188 for (int i = 0; i < nTimes; i++) { 189 mRateLimiter.tryAcquireApiCallQuota(UID, quotaCategory, isInForeground, memoryCost); 190 } 191 } 192 } 193