| OLD | NEW |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 package org.chromium.chrome.browser.offlinepages; | 5 package org.chromium.chrome.browser.offlinepages; |
| 6 | 6 |
| 7 import android.app.NotificationManager; | 7 import android.app.NotificationManager; |
| 8 import android.content.Context; | 8 import android.content.Context; |
| 9 import android.os.Environment; | 9 import android.os.Environment; |
| 10 import android.text.TextUtils; | 10 import android.text.TextUtils; |
| 11 import android.util.LongSparseArray; | 11 import android.util.LongSparseArray; |
| 12 | 12 |
| 13 import org.junit.After; |
| 14 import org.junit.Assert; |
| 15 import org.junit.Before; |
| 16 import org.junit.Rule; |
| 17 import org.junit.Test; |
| 18 import org.junit.runner.RunWith; |
| 19 |
| 13 import org.chromium.base.Callback; | 20 import org.chromium.base.Callback; |
| 14 import org.chromium.base.ContextUtils; | 21 import org.chromium.base.ContextUtils; |
| 15 import org.chromium.base.Log; | 22 import org.chromium.base.Log; |
| 16 import org.chromium.base.ThreadUtils; | 23 import org.chromium.base.ThreadUtils; |
| 17 import org.chromium.base.test.util.CommandLineFlags; | 24 import org.chromium.base.test.util.CommandLineFlags; |
| 18 import org.chromium.base.test.util.Manual; | 25 import org.chromium.base.test.util.Manual; |
| 19 import org.chromium.base.test.util.TimeoutScale; | 26 import org.chromium.base.test.util.TimeoutScale; |
| 20 import org.chromium.chrome.browser.ChromeActivity; | 27 import org.chromium.chrome.browser.ChromeActivity; |
| 28 import org.chromium.chrome.browser.ChromeSwitches; |
| 21 import org.chromium.chrome.browser.offlinepages.evaluation.OfflinePageEvaluation
Bridge; | 29 import org.chromium.chrome.browser.offlinepages.evaluation.OfflinePageEvaluation
Bridge; |
| 22 import org.chromium.chrome.browser.offlinepages.evaluation.OfflinePageEvaluation
Bridge.OfflinePageEvaluationObserver; | 30 import org.chromium.chrome.browser.offlinepages.evaluation.OfflinePageEvaluation
Bridge.OfflinePageEvaluationObserver; |
| 23 import org.chromium.chrome.browser.profiles.Profile; | 31 import org.chromium.chrome.browser.profiles.Profile; |
| 24 import org.chromium.chrome.test.ChromeActivityTestCaseBase; | 32 import org.chromium.chrome.test.ChromeActivityTestRule; |
| 33 import org.chromium.chrome.test.ChromeJUnit4ClassRunner; |
| 25 import org.chromium.components.offlinepages.BackgroundSavePageResult; | 34 import org.chromium.components.offlinepages.BackgroundSavePageResult; |
| 26 | 35 |
| 27 import java.io.BufferedReader; | 36 import java.io.BufferedReader; |
| 28 import java.io.File; | 37 import java.io.File; |
| 29 import java.io.FileInputStream; | 38 import java.io.FileInputStream; |
| 30 import java.io.FileNotFoundException; | 39 import java.io.FileNotFoundException; |
| 31 import java.io.FileReader; | 40 import java.io.FileReader; |
| 32 import java.io.FileWriter; | 41 import java.io.FileWriter; |
| 33 import java.io.IOException; | 42 import java.io.IOException; |
| 34 import java.io.InputStream; | 43 import java.io.InputStream; |
| 35 import java.io.OutputStreamWriter; | 44 import java.io.OutputStreamWriter; |
| 36 | |
| 37 import java.util.ArrayList; | 45 import java.util.ArrayList; |
| 38 import java.util.List; | 46 import java.util.List; |
| 39 import java.util.Properties; | 47 import java.util.Properties; |
| 40 import java.util.concurrent.CountDownLatch; | 48 import java.util.concurrent.CountDownLatch; |
| 41 import java.util.concurrent.Semaphore; | 49 import java.util.concurrent.Semaphore; |
| 42 import java.util.concurrent.TimeUnit; | 50 import java.util.concurrent.TimeUnit; |
| 43 | 51 |
| 44 /** | 52 /** |
| 45 * Tests OfflinePageBridge.SavePageLater over a batch of urls. | 53 * Tests OfflinePageBridge.SavePageLater over a batch of urls. |
| 46 * Tests against a list of top EM urls, try to call SavePageLater on each of the
url. It also | 54 * Tests against a list of top EM urls, try to call SavePageLater on each of the
url. It also |
| 47 * record metrics (failure rate, time elapsed etc.) by writing metrics to a file
on external | 55 * record metrics (failure rate, time elapsed etc.) by writing metrics to a file
on external |
| 48 * storage. This will always use prerenderer. | 56 * storage. This will always use prerenderer. |
| 49 */ | 57 */ |
| 50 @CommandLineFlags.Add({"disable-features=BackgroundLoader"}) | 58 @RunWith(ChromeJUnit4ClassRunner.class) |
| 51 public class OfflinePageSavePageLaterEvaluationTest | 59 @CommandLineFlags.Add({"disable-features=BackgroundLoader", |
| 52 extends ChromeActivityTestCaseBase<ChromeActivity> { | 60 ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE, |
| 61 ChromeActivityTestRule.DISABLE_NETWORK_PREDICTION_FLAG}) |
| 62 public class OfflinePageSavePageLaterEvaluationTest { |
| 53 /** | 63 /** |
| 54 * Class which is used to calculate time difference. | 64 * Class which is used to calculate time difference. |
| 55 */ | 65 */ |
| 66 |
| 67 @Rule |
| 68 public ChromeActivityTestRule<ChromeActivity> mActivityTestRule = |
| 69 new ChromeActivityTestRule<>(ChromeActivity.class); |
| 70 |
| 56 static class TimeDelta { | 71 static class TimeDelta { |
| 57 public void setStartTime(Long startTime) { | 72 public void setStartTime(Long startTime) { |
| 58 mStartTime = startTime; | 73 mStartTime = startTime; |
| 59 } | 74 } |
| 60 public void setEndTime(Long endTime) { | 75 public void setEndTime(Long endTime) { |
| 61 mEndTime = endTime; | 76 mEndTime = endTime; |
| 62 } | 77 } |
| 63 // Return time delta in milliseconds. | 78 // Return time delta in milliseconds. |
| 64 public Long getTimeDelta() { | 79 public Long getTimeDelta() { |
| 65 return mEndTime - mStartTime; | 80 return mEndTime - mStartTime; |
| (...skipping 30 matching lines...) Expand all Loading... |
| 96 private CountDownLatch mCompletionLatch; | 111 private CountDownLatch mCompletionLatch; |
| 97 private List<String> mUrls; | 112 private List<String> mUrls; |
| 98 private int mCount; | 113 private int mCount; |
| 99 private boolean mIsUserRequested; | 114 private boolean mIsUserRequested; |
| 100 private boolean mUseTestScheduler; | 115 private boolean mUseTestScheduler; |
| 101 private int mScheduleBatchSize; | 116 private int mScheduleBatchSize; |
| 102 private boolean mUseBackgroundLoader; | 117 private boolean mUseBackgroundLoader; |
| 103 | 118 |
| 104 private LongSparseArray<RequestMetadata> mRequestMetadata; | 119 private LongSparseArray<RequestMetadata> mRequestMetadata; |
| 105 | 120 |
| 106 public OfflinePageSavePageLaterEvaluationTest() { | 121 @Before |
| 107 super(ChromeActivity.class); | 122 public void setUp() throws Exception { |
| 108 } | 123 mActivityTestRule.startMainActivityOnBlankPage(); |
| 109 | |
| 110 @Override | |
| 111 protected void setUp() throws Exception { | |
| 112 super.setUp(); | |
| 113 mRequestMetadata = new LongSparseArray<RequestMetadata>(); | 124 mRequestMetadata = new LongSparseArray<RequestMetadata>(); |
| 114 mCount = 0; | 125 mCount = 0; |
| 115 } | 126 } |
| 116 | 127 |
| 117 @Override | 128 @After |
| 118 protected void tearDown() throws Exception { | 129 public void tearDown() throws Exception { |
| 119 NotificationManager notificationManager = | 130 NotificationManager notificationManager = |
| 120 (NotificationManager) ContextUtils.getApplicationContext().getSy
stemService( | 131 (NotificationManager) ContextUtils.getApplicationContext().getSy
stemService( |
| 121 Context.NOTIFICATION_SERVICE); | 132 Context.NOTIFICATION_SERVICE); |
| 122 notificationManager.cancelAll(); | 133 notificationManager.cancelAll(); |
| 123 final Semaphore mClearingSemaphore = new Semaphore(0); | 134 final Semaphore mClearingSemaphore = new Semaphore(0); |
| 124 ThreadUtils.runOnUiThread(new Runnable() { | 135 ThreadUtils.runOnUiThread(new Runnable() { |
| 125 @Override | 136 @Override |
| 126 public void run() { | 137 public void run() { |
| 127 assert mBridge != null; | 138 assert mBridge != null; |
| 128 mBridge.getRequestsInQueue(new Callback<SavePageRequest[]>() { | 139 mBridge.getRequestsInQueue(new Callback<SavePageRequest[]>() { |
| (...skipping 10 matching lines...) Expand all Loading... |
| 139 } | 150 } |
| 140 }); | 151 }); |
| 141 } | 152 } |
| 142 }); | 153 }); |
| 143 } | 154 } |
| 144 }); | 155 }); |
| 145 checkTrue(mClearingSemaphore.tryAcquire(REMOVE_REQUESTS_TIMEOUT_MS, Time
Unit.MILLISECONDS), | 156 checkTrue(mClearingSemaphore.tryAcquire(REMOVE_REQUESTS_TIMEOUT_MS, Time
Unit.MILLISECONDS), |
| 146 "Timed out when clearing remaining requests!"); | 157 "Timed out when clearing remaining requests!"); |
| 147 mBridge.closeLog(); | 158 mBridge.closeLog(); |
| 148 mBridge.destroy(); | 159 mBridge.destroy(); |
| 149 super.tearDown(); | |
| 150 } | |
| 151 | |
| 152 @Override | |
| 153 public void startMainActivity() throws InterruptedException { | |
| 154 startMainActivityOnBlankPage(); | |
| 155 } | 160 } |
| 156 | 161 |
| 157 /** | 162 /** |
| 158 * Get a reader for a given input file path. | 163 * Get a reader for a given input file path. |
| 159 */ | 164 */ |
| 160 private BufferedReader getInputStream(String inputFilePath) throws FileNotFo
undException { | 165 private BufferedReader getInputStream(String inputFilePath) throws FileNotFo
undException { |
| 161 FileReader fileReader = | 166 FileReader fileReader = |
| 162 new FileReader(new File(Environment.getExternalStorageDirectory(
), inputFilePath)); | 167 new FileReader(new File(Environment.getExternalStorageDirectory(
), inputFilePath)); |
| 163 BufferedReader bufferedReader = new BufferedReader(fileReader); | 168 BufferedReader bufferedReader = new BufferedReader(fileReader); |
| 164 return bufferedReader; | 169 return bufferedReader; |
| (...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 205 private void log(String tag, String format, Object... args) { | 210 private void log(String tag, String format, Object... args) { |
| 206 mBridge.log(tag, String.format(format, args)); | 211 mBridge.log(tag, String.format(format, args)); |
| 207 } | 212 } |
| 208 | 213 |
| 209 /** | 214 /** |
| 210 * Assert the condition is true, otherwise abort the test and log. | 215 * Assert the condition is true, otherwise abort the test and log. |
| 211 */ | 216 */ |
| 212 private void checkTrue(boolean condition, String message) { | 217 private void checkTrue(boolean condition, String message) { |
| 213 if (!condition) { | 218 if (!condition) { |
| 214 log(TAG, message); | 219 log(TAG, message); |
| 215 fail(); | 220 Assert.fail(); |
| 216 } | 221 } |
| 217 } | 222 } |
| 218 | 223 |
| 219 /** | 224 /** |
| 220 * Initializes the evaluation bridge which will be used. | 225 * Initializes the evaluation bridge which will be used. |
| 221 * @param useCustomScheduler True if customized scheduler (the one with imme
diate scheduling) | 226 * @param useCustomScheduler True if customized scheduler (the one with imme
diate scheduling) |
| 222 * will be used. False otherwise. | 227 * will be used. False otherwise. |
| 223 * @param useBackgroundLoader True if use background loader. False if use pr
erenderer. | 228 * @param useBackgroundLoader True if use background loader. False if use pr
erenderer. |
| 224 */ | 229 */ |
| 225 private void initializeBridgeForProfile(final boolean useTestingScheduler, | 230 private void initializeBridgeForProfile(final boolean useTestingScheduler, |
| 226 final boolean useBackgroundLoader) throws InterruptedException { | 231 final boolean useBackgroundLoader) throws InterruptedException { |
| 227 final Semaphore semaphore = new Semaphore(0); | 232 final Semaphore semaphore = new Semaphore(0); |
| 228 ThreadUtils.runOnUiThread(new Runnable() { | 233 ThreadUtils.runOnUiThread(new Runnable() { |
| 229 @Override | 234 @Override |
| 230 public void run() { | 235 public void run() { |
| 231 Profile profile = Profile.getLastUsedProfile(); | 236 Profile profile = Profile.getLastUsedProfile(); |
| 232 mBridge = new OfflinePageEvaluationBridge( | 237 mBridge = new OfflinePageEvaluationBridge( |
| 233 profile, useTestingScheduler, useBackgroundLoader); | 238 profile, useTestingScheduler, useBackgroundLoader); |
| 234 if (mBridge == null) { | 239 if (mBridge == null) { |
| 235 fail("OfflinePageEvaluationBridge initialization failed!"); | 240 Assert.fail("OfflinePageEvaluationBridge initialization fail
ed!"); |
| 236 return; | 241 return; |
| 237 } | 242 } |
| 238 if (mBridge.isOfflinePageModelLoaded()) { | 243 if (mBridge.isOfflinePageModelLoaded()) { |
| 239 semaphore.release(); | 244 semaphore.release(); |
| 240 return; | 245 return; |
| 241 } | 246 } |
| 242 mBridge.addObserver(new OfflinePageEvaluationObserver() { | 247 mBridge.addObserver(new OfflinePageEvaluationObserver() { |
| 243 @Override | 248 @Override |
| 244 public void offlinePageModelLoaded() { | 249 public void offlinePageModelLoaded() { |
| 245 semaphore.release(); | 250 semaphore.release(); |
| (...skipping 79 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 325 ThreadUtils.runOnUiThread(new Runnable() { | 330 ThreadUtils.runOnUiThread(new Runnable() { |
| 326 @Override | 331 @Override |
| 327 public void run() { | 332 public void run() { |
| 328 mBridge.savePageLater(url, namespace, mIsUserRequested); | 333 mBridge.savePageLater(url, namespace, mIsUserRequested); |
| 329 } | 334 } |
| 330 }); | 335 }); |
| 331 } | 336 } |
| 332 | 337 |
| 333 private void processUrls(List<String> urls) throws InterruptedException, IOE
xception { | 338 private void processUrls(List<String> urls) throws InterruptedException, IOE
xception { |
| 334 if (mBridge == null) { | 339 if (mBridge == null) { |
| 335 fail("Test initialization error, aborting. No results would be writt
en."); | 340 Assert.fail("Test initialization error, aborting. No results would b
e written."); |
| 336 return; | 341 return; |
| 337 } | 342 } |
| 338 int count = 0; | 343 int count = 0; |
| 339 log(TAG_PROGRESS, "# of Urls in file: " + mUrls.size()); | 344 log(TAG_PROGRESS, "# of Urls in file: " + mUrls.size()); |
| 340 for (int i = 0; i < mUrls.size(); i++) { | 345 for (int i = 0; i < mUrls.size(); i++) { |
| 341 savePageLater(mUrls.get(i), NAMESPACE); | 346 savePageLater(mUrls.get(i), NAMESPACE); |
| 342 count++; | 347 count++; |
| 343 if (count == mScheduleBatchSize || i == mUrls.size() - 1) { | 348 if (count == mScheduleBatchSize || i == mUrls.size() - 1) { |
| 344 count = 0; | 349 count = 0; |
| 345 mCompletionLatch = new CountDownLatch(1); | 350 mCompletionLatch = new CountDownLatch(1); |
| (...skipping 16 matching lines...) Expand all Loading... |
| 362 mUrls.add(url); | 367 mUrls.add(url); |
| 363 } | 368 } |
| 364 } | 369 } |
| 365 } finally { | 370 } finally { |
| 366 if (bufferedReader != null) { | 371 if (bufferedReader != null) { |
| 367 bufferedReader.close(); | 372 bufferedReader.close(); |
| 368 } | 373 } |
| 369 } | 374 } |
| 370 } catch (FileNotFoundException e) { | 375 } catch (FileNotFoundException e) { |
| 371 Log.e(TAG, e.getMessage(), e); | 376 Log.e(TAG, e.getMessage(), e); |
| 372 fail(String.format("URL file %s is not found.", inputFilePath)); | 377 Assert.fail(String.format("URL file %s is not found.", inputFilePath
)); |
| 373 } | 378 } |
| 374 } | 379 } |
| 375 | 380 |
| 376 // Translate the int value of status to BackgroundSavePageResult. | 381 // Translate the int value of status to BackgroundSavePageResult. |
| 377 private String statusToString(int status) { | 382 private String statusToString(int status) { |
| 378 switch (status) { | 383 switch (status) { |
| 379 case BackgroundSavePageResult.SUCCESS: | 384 case BackgroundSavePageResult.SUCCESS: |
| 380 return "SUCCESS"; | 385 return "SUCCESS"; |
| 381 case BackgroundSavePageResult.LOADING_FAILURE: | 386 case BackgroundSavePageResult.LOADING_FAILURE: |
| 382 return "LOADING_FAILURE"; | 387 return "LOADING_FAILURE"; |
| (...skipping 107 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 490 File configFile = new File(Environment.getExternalStorageDirectory()
, CONFIG_FILE_PATH); | 495 File configFile = new File(Environment.getExternalStorageDirectory()
, CONFIG_FILE_PATH); |
| 491 inputStream = new FileInputStream(configFile); | 496 inputStream = new FileInputStream(configFile); |
| 492 properties.load(inputStream); | 497 properties.load(inputStream); |
| 493 mIsUserRequested = Boolean.parseBoolean(properties.getProperty("IsUs
erRequested")); | 498 mIsUserRequested = Boolean.parseBoolean(properties.getProperty("IsUs
erRequested")); |
| 494 mUseTestScheduler = Boolean.parseBoolean(properties.getProperty("Use
TestScheduler")); | 499 mUseTestScheduler = Boolean.parseBoolean(properties.getProperty("Use
TestScheduler")); |
| 495 mScheduleBatchSize = Integer.parseInt(properties.getProperty("Schedu
leBatchSize")); | 500 mScheduleBatchSize = Integer.parseInt(properties.getProperty("Schedu
leBatchSize")); |
| 496 mUseBackgroundLoader = | 501 mUseBackgroundLoader = |
| 497 Boolean.parseBoolean(properties.getProperty("UseBackgroundLo
ader")); | 502 Boolean.parseBoolean(properties.getProperty("UseBackgroundLo
ader")); |
| 498 } catch (FileNotFoundException e) { | 503 } catch (FileNotFoundException e) { |
| 499 Log.e(TAG, e.getMessage(), e); | 504 Log.e(TAG, e.getMessage(), e); |
| 500 fail(String.format( | 505 Assert.fail(String.format( |
| 501 "Config file %s is not found, aborting the test.", CONFIG_FI
LE_PATH)); | 506 "Config file %s is not found, aborting the test.", CONFIG_FI
LE_PATH)); |
| 502 } catch (NumberFormatException e) { | 507 } catch (NumberFormatException e) { |
| 503 Log.e(TAG, e.getMessage(), e); | 508 Log.e(TAG, e.getMessage(), e); |
| 504 fail("Error parsing config file, aborting test."); | 509 Assert.fail("Error parsing config file, aborting test."); |
| 505 } finally { | 510 } finally { |
| 506 if (inputStream != null) { | 511 if (inputStream != null) { |
| 507 inputStream.close(); | 512 inputStream.close(); |
| 508 } | 513 } |
| 509 } | 514 } |
| 510 } | 515 } |
| 511 | 516 |
| 512 /** | 517 /** |
| 513 * The test is the entry point for all kinds of testing of SavePageLater. | 518 * The test is the entry point for all kinds of testing of SavePageLater. |
| 514 * It is encouraged to use run_offline_page_evaluation_test.py to run this t
est. | 519 * It is encouraged to use run_offline_page_evaluation_test.py to run this t
est. |
| 515 * TimeoutScale is set to 4, in case we hit the hard limit for @Manual tests
(10 hours) | 520 * TimeoutScale is set to 4, in case we hit the hard limit for @Manual tests
(10 hours) |
| 516 * and gets killed. It expand the timeout to 10 * 4 hours. | 521 * and gets killed. It expand the timeout to 10 * 4 hours. |
| 517 * We won't be treating svelte devices differently so enable the feature whi
ch would let | 522 * We won't be treating svelte devices differently so enable the feature whi
ch would let |
| 518 * immediate processing also works on svelte devices. This flag will *not* a
ffect normal | 523 * immediate processing also works on svelte devices. This flag will *not* a
ffect normal |
| 519 * devices. | 524 * devices. |
| 520 */ | 525 */ |
| 526 @Test |
| 521 @Manual | 527 @Manual |
| 522 @TimeoutScale(4) | 528 @TimeoutScale(4) |
| 523 @CommandLineFlags | 529 @CommandLineFlags.Add({"enable-features=OfflinePagesSvelteConcurrentLoading"
}) |
| 524 .Add({"enable-features=OfflinePagesSvelteConcurrentLoading"}) | |
| 525 @CommandLineFlags.Remove({"disable-features=OfflinePagesSvelteConcurrentLoad
ing"}) | 530 @CommandLineFlags.Remove({"disable-features=OfflinePagesSvelteConcurrentLoad
ing"}) |
| 526 public void testFailureRate() throws IOException, InterruptedException { | 531 public void testFailureRate() throws IOException, InterruptedException { |
| 527 parseConfigFile(); | 532 parseConfigFile(); |
| 528 setUpIOAndBridge(mUseTestScheduler, mUseBackgroundLoader); | 533 setUpIOAndBridge(mUseTestScheduler, mUseBackgroundLoader); |
| 529 processUrls(mUrls); | 534 processUrls(mUrls); |
| 530 } | 535 } |
| 531 | 536 |
| 532 /** | 537 /** |
| 533 * Runs testFailureRate with background loader enabled. | 538 * Runs testFailureRate with background loader enabled. |
| 534 * We won't be treating svelte devices differently so enable the feature whi
ch would let | 539 * We won't be treating svelte devices differently so enable the feature whi
ch would let |
| 535 * immediate processing also works on svelte devices. This flag will *not* a
ffect normal | 540 * immediate processing also works on svelte devices. This flag will *not* a
ffect normal |
| 536 * devices. | 541 * devices. |
| 537 */ | 542 */ |
| 543 @Test |
| 538 @Manual | 544 @Manual |
| 539 @TimeoutScale(4) | 545 @TimeoutScale(4) |
| 540 @CommandLineFlags | 546 @CommandLineFlags.Add({"enable-features=BackgroundLoaderOfflinePagesSvelteCo
ncurrentLoading"}) |
| 541 .Add({"enable-features=BackgroundLoader,OfflinePagesSvelteConcurrent
Loading"}) | 547 @CommandLineFlags.Remove({ |
| 542 @CommandLineFlags | 548 "disable-features=BackgroundLoaderOfflinePagesSvelteConcurrentLoadin
g"}) |
| 543 .Remove({"disable-features=BackgroundLoader,OfflinePagesSvelteConcur
rentLoading"}) | |
| 544 public void testBackgroundLoaderFailureRate() throws IOException, Interrupte
dException { | 549 public void testBackgroundLoaderFailureRate() throws IOException, Interrupte
dException { |
| 545 testFailureRate(); | 550 testFailureRate(); |
| 546 } | 551 } |
| 547 } | 552 } |
| OLD | NEW |