• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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