/* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.firebase.database; import static com.cedarsoftware.util.DeepEquals.deepEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.google.firebase.FirebaseApp; import com.google.firebase.database.core.CoreTestHelpers; import com.google.firebase.database.core.DatabaseConfig; import com.google.firebase.database.core.Path; import com.google.firebase.database.core.RepoManager; import com.google.firebase.database.core.view.QuerySpec; import com.google.firebase.database.future.WriteFuture; import com.google.firebase.database.snapshot.ChildKey; import com.google.firebase.database.util.JsonMapper; import com.google.firebase.database.utilities.DefaultRunLoop; import com.google.firebase.internal.NonNull; import com.google.firebase.testing.TestUtils; import java.io.IOException; import java.lang.Thread.UncaughtExceptionHandler; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; public class TestHelpers { public static DatabaseConfig newFrozenTestConfig(FirebaseApp app) { DatabaseConfig cfg = newTestConfig(app); CoreTestHelpers.freezeContext(cfg); return cfg; } public static DatabaseConfig newTestConfig(FirebaseApp app) { DatabaseConfig config = new DatabaseConfig(); config.setLogLevel(Logger.Level.WARN); config.setFirebaseApp(app); return config; } public static void interruptConfig(final DatabaseConfig config) throws InterruptedException { RepoManager.interrupt(config); long now = System.currentTimeMillis(); synchronized (config) { while (System.currentTimeMillis() - now < TestUtils.TEST_TIMEOUT_MILLIS) { if (config.isStopped()) { break; } config.wait(10); } } } public static DatabaseConfig getDatabaseConfig(FirebaseApp app) { return FirebaseDatabase.getInstance(app).getConfig(); } public static ScheduledExecutorService getExecutorService(DatabaseConfig config) { DefaultRunLoop runLoop = (DefaultRunLoop) config.getRunLoop(); return runLoop.getExecutorService(); } public static void setLogger( DatabaseConfig ctx, com.google.firebase.database.logging.Logger logger) { ctx.setLogger(logger); } public static void waitFor(Semaphore semaphore) throws InterruptedException { waitFor(semaphore, 1); } public static void waitFor(Semaphore semaphore, int count) throws InterruptedException { waitFor(semaphore, count, TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); } public static void waitFor(Semaphore semaphore, int count, long timeout, TimeUnit unit) throws InterruptedException { boolean success = semaphore.tryAcquire(count, timeout, unit); assertTrue("Operation timed out", success); } public static DataSnapshot getSnap(Query ref) throws InterruptedException { final Semaphore semaphore = new Semaphore(0); // Hack to get around final reference issue final List<DataSnapshot> snapshotList = new ArrayList<>(1); ref.addListenerForSingleValueEvent( new ValueEventListener() { @Override public void onDataChange(DataSnapshot snapshot) { snapshotList.add(snapshot); semaphore.release(1); } @Override public void onCancelled(DatabaseError error) { semaphore.release(1); } }); semaphore.tryAcquire(1, TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); return snapshotList.get(0); } public static Map<String, Object> fromJsonString(String json) { try { return JsonMapper.parseJson(json); } catch (IOException e) { throw new RuntimeException(e); } } public static Map<String, Object> fromSingleQuotedString(String json) { return fromJsonString(json.replace("'", "\"")); } public static void waitForRoundtrip(DatabaseReference reader) { try { new WriteFuture(reader.getRoot().child(UUID.randomUUID().toString()), null, null).timedGet(); } catch (Exception e) { throw new RuntimeException(e); } } public static void waitForQueue(DatabaseReference ref) { try { final Semaphore semaphore = new Semaphore(0); ref.getRepo() .scheduleNow( new Runnable() { @Override public void run() { semaphore.release(); } }); semaphore.acquire(); } catch (Exception e) { throw new RuntimeException(e); } } public static String repeatedString(String s, int n) { StringBuilder result = new StringBuilder(""); for (int i = 0; i < n; i++) { result.append(s); } return result.toString(); } // Create a (test) object which places a test value at the end of the // object path (e.g., a/b/c would yield {a: {b: {c: "test_value"}}} public static Map<String, Object> buildObjFromPath(Path path, Object testValue) { final HashMap<String, Object> result = new HashMap<>(); HashMap<String, Object> parent = result; for (Iterator<ChildKey> i = path.iterator(); i.hasNext(); ) { ChildKey key = i.next(); if (i.hasNext()) { HashMap<String, Object> child = new HashMap<>(); parent.put(key.asString(), child); parent = child; } else { parent.put(key.asString(), testValue); } } return result; } // Lookup the value at the path in HashMap (e.g., "a/b/c"). public static Object applyPath(Object value, Path path) { for (ChildKey key : path) { value = ((Map) value).get(key.asString()); } return value; } public static void assertContains(String str, String substr) { assertTrue("'" + str + "' does not contain '" + substr + "'.", str.contains(substr)); } public static ChildKey ck(String childKey) { return ChildKey.fromString(childKey); } public static Path path(String path) { return new Path(path); } public static QuerySpec defaultQueryAt(String path) { return QuerySpec.defaultQueryAtPath(new Path(path)); } public static Set<ChildKey> childKeySet(String... stringKeys) { Set<ChildKey> childKeys = new HashSet<>(); for (String k : stringKeys) { childKeys.add(ChildKey.fromString(k)); } return childKeys; } public static void setHijackHash(DatabaseReference ref, boolean hijackHash) { ref.setHijackHash(hijackHash); } /** * Deeply compares two (2) objects. This method will call any overridden equals() methods if they * exist. If not, it will then proceed to do a field-by-field comparison, and when a non-primitive * field is encountered, recursively continue the deep comparison. When an array is found, it will * also ensure that the array contents are deeply equal, not requiring the array instance * (container) to be identical. This method will successfully compare object graphs that have * cycles (A->B->C->A). There is no need to ever use the Arrays.deepEquals() method as this is * a true and more effective super set. */ public static void assertDeepEquals(Object a, Object b) { if (!deepEquals(a, b)) { fail("Values different.\nExpected: " + a + "\nActual: " + b); } } /** * Instruments the given FirebaseApp instance to catch exceptions that are thrown by background * threads. More specifically, it registers error handlers with the RunLoop and EventTarget * of the FirebaseDatabase. These components run asynchronously, and therefore any exceptions * (including assertion failures) encountered by them do not typically cause the test runner * to fail. The error handlers added by this method help to catch those exceptions, and * propagate them to the test runner's main thread, thus causing tests to fail on async errors. * Integration tests, particularly the ones that interact with FirebaseDatabase, should * call this method in a Before test fixture. * * @param app A FirebaseApp instance to be instrumented */ public static void wrapForErrorHandling(@NonNull FirebaseApp app) { DatabaseConfig context = getDatabaseConfig(app); CoreTestHelpers.freezeContext(context); DefaultRunLoop runLoop = (DefaultRunLoop) context.getRunLoop(); runLoop.setExceptionHandler(new TestExceptionHandler()); CoreTestHelpers.setEventTargetExceptionHandler(context, new TestExceptionHandler()); } /** * Checks to see if any asynchronous error handlers added to the given FirebaseApp instance * have been activated. If so, this method will re-throw the root cause exception as a new * RuntimeException. Finally, this method also removes any error handlers added previously by * the wrapForErrorHandling method. Invoke this method in integration tests from an After * test fixture. * * @param app AFireabseApp instance already instrumented by wrapForErrorHandling */ public static void assertAndUnwrapErrorHandlers(FirebaseApp app) { DatabaseConfig context = getDatabaseConfig(app); DefaultRunLoop runLoop = (DefaultRunLoop) context.getRunLoop(); try { TestExceptionHandler handler = (TestExceptionHandler) runLoop.getExceptionHandler(); Throwable error = handler.throwable.get(); if (error != null) { throw new RuntimeException(error); } handler = (TestExceptionHandler) CoreTestHelpers.getEventTargetExceptionHandler(context); error = handler.throwable.get(); if (error != null) { throw new RuntimeException(error); } } finally { CoreTestHelpers.setEventTargetExceptionHandler(context, null); runLoop.setExceptionHandler(null); } } public static void assertTimeDelta(long timestamp) { assertTrue(Math.abs(System.currentTimeMillis() - timestamp) < TestUtils.TEST_TIMEOUT_MILLIS); } private static class TestExceptionHandler implements UncaughtExceptionHandler { private final AtomicReference<Throwable> throwable = new AtomicReference<>(); @Override public void uncaughtException(Thread t, Throwable e) { throwable.compareAndSet(null, e); } } }