on android device via junit4 instrumentation, want test realm classes.
but java.lang.illegalstateexception: realm opened thread without looper. async queries need handler send results of query
private final realm realm; //this constructed on instrumentation thread during testing, in production it's constructed on main thread. public observable<localcart> getcart() { return realm.where(realmcart.class) .equalto(realmcart.done, true) .findallasync() //throws thread w/o looper exception, bc instrumentation thread not have looper .asobservable() .map(realmresults::first) .map(localcart::create); }
how can achieve goal? test:
cartdb = new cartdbimpl(datatestdelegate.realm()); cartdb.write(expected); testsubscriber<localcart> test = new testsubscriber<>(); cartdb.getcart() .subscribe(test); //exception thrown here assertions.assertthat(test).hasreceivedvalues(expected);
two solutions:
1.) replace realm configuration in application inmemory()
realm test
@before public void setup() { instrumentation instrumentation = instrumentationregistry.getinstrumentation(); instrumentation.runonmainsync(new runnable() { @override public void run() { applicationcomponent applicationcomponent = injector.instance.getapplicationcomponent(); appconfig appconfig = applicationcomponent.appconfig(); customapplication customapplication = applicationcomponent.application(); appconfig.setdefaultrealmconfig(new realmconfiguration.builder(customapplication).inmemory() .deleterealmifmigrationneeded() .build()); if(customapplication.getrealm() != null && !customapplication.getrealm().isclosed()) { customapplication.getrealm().close(); } customapplication.initializerealm(); } });
and use
instrumentation instrumentation = instrumentationregistry.getinstrumentation(); instrumentation.runonmainsync(() -> { // ... });
2.) try somehow make these following classes work snatched realm library instrumentation tests while ago. went 1) because kinda gave on after while, i'm pretty sure worked realm, shouldn't impossible.
/* * copyright 2014 realm inc. * * licensed under apache license, version 2.0 (the "license"); * may not use file except in compliance license. * may obtain copy of license @ * * http://www.apache.org/licenses/license-2.0 * * unless required applicable law or agreed in writing, software * distributed under license distributed on "as is" basis, * without warranties or conditions of kind, either express or implied. * see license specific language governing permissions , * limitations under license. */ package io.realm; import android.os.looper; import java.io.unsupportedencodingexception; import java.security.messagedigest; import java.security.nosuchalgorithmexception; import java.util.random; import java.util.concurrent.countdownlatch; import java.util.concurrent.executorservice; import java.util.concurrent.timeunit; import java.util.concurrent.atomic.atomicreference; import static junit.framework.assert.fail; public class testhelper { // returns random key used encrypted realms. public static byte[] getrandomkey() { byte[] key = new byte[64]; new random().nextbytes(key); return key; } // returns random key given seed. used encrypted realms. public static byte[] getrandomkey(long seed) { byte[] key = new byte[64]; new random(seed).nextbytes(key); return key; } // alloc garbage can. pass maxsize = 0 use it. public static byte[] allocgarbage(int garbagesize) { if(garbagesize == 0) { long maxmemory = runtime.getruntime().maxmemory(); long totalmemory = runtime.getruntime().totalmemory(); garbagesize = (int) (maxmemory - totalmemory) / 10 * 9; } byte garbage[] = new byte[0]; try { if(garbagesize > 0) { garbage = new byte[garbagesize]; garbage[0] = 1; garbage[garbage.length - 1] = 1; } } catch(outofmemoryerror oom) { return allocgarbage(garbagesize / 10 * 9); } return garbage; } // creates sha512 hash of string. can used password encrypted realms. public static byte[] sha512(string str) { try { messagedigest md = messagedigest.getinstance("sha-512"); md.update(str.getbytes("utf-8"), 0, str.length()); return md.digest(); } catch(nosuchalgorithmexception e) { throw new runtimeexception(e); } catch(unsupportedencodingexception e) { throw new runtimeexception(e); } } public static void awaitorfail(countdownlatch latch) { awaitorfail(latch, 30); } public static void awaitorfail(countdownlatch latch, int numberofseconds) { try { if(!latch.await(numberofseconds, timeunit.seconds)) { fail("test took longer " + numberofseconds + " seconds"); } } catch(interruptedexception e) { fail(e.getmessage()); } } // clean resource, shutdown executor service & throw background exception public static void exitorthrow(final executorservice executorservice, final countdownlatch signaltestfinished, final countdownlatch signalclosedrealm, final looper[] looper, final throwable[] throwable) throws throwable { // wait signal indicating test's use case done try { // if fails want try hard possible cleanup. if fail close resources // properly, `after()` method throw because tries delete realms // used. exception in `after()` code mask original error. testhelper.awaitorfail(signaltestfinished); } { // close executor executorservice.shutdownnow(); if(looper[0] != null) { // failing quit looper not execute block responsible // of closing realm looper[0].quit(); } // wait block execute & close realm testhelper.awaitorfail(signalclosedrealm); if(throwable[0] != null) { // throw assertion errors happened in background thread throw throwable[0]; } } } public static abstract class task { public abstract void run() throws exception; } public static void executeonnonlooperthread(final task task) throws throwable { final atomicreference<throwable> thrown = new atomicreference<throwable>(); final thread thread = new thread() { @override public void run() { try { task.run(); } catch(throwable e) { thrown.set(e); if(e instanceof error) { throw (error) e; } } } }; thread.start(); thread.join(); final throwable throwable = thrown.get(); if(throwable != null) { throw throwable; } } } /* * copyright 2016 realm inc. * * licensed under apache license, version 2.0 (the "license"); * may not use file except in compliance license. * may obtain copy of license @ * * http://www.apache.org/licenses/license-2.0 * * unless required applicable law or agreed in writing, software * distributed under license distributed on "as is" basis, * without warranties or conditions of kind, either express or implied. * see license specific language governing permissions , * limitations under license. */ package io.realm.rule; import android.content.context; import android.content.res.assetmanager; import org.junit.rules.temporaryfolder; import org.junit.runner.description; import org.junit.runners.model.statement; import java.io.file; import java.io.fileoutputstream; import java.io.ioexception; import java.io.inputstream; import java.util.collections; import java.util.map; import java.util.set; import java.util.concurrent.concurrenthashmap; import io.realm.realm; import io.realm.realmconfiguration; import static org.junit.assert.asserttrue; /** * rule creates {@link realmconfiguration } in temporary directory , deletes realm created * configuration once test finishes. sure close realm instances before finishing test. otherwise * {@link realm#deleterealm(realmconfiguration)} throw exception in {@link #after()} method. * temp directory deleted regardless if {@link realm#deleterealm(realmconfiguration)} fails or not. */ public class testrealmconfigurationfactory extends temporaryfolder { private map<realmconfiguration, boolean> map = new concurrenthashmap<realmconfiguration, boolean>(); private set<realmconfiguration> configurations = collections.newsetfrommap(map); protected boolean unittestfailed = false; @override public statement apply(final statement base, description description) { return new statement() { @override public void evaluate() throws throwable { before(); try { base.evaluate(); } catch(throwable throwable) { unittestfailed = true; throw throwable; } { after(); } } }; } @override protected void before() throws throwable { super.before(); } @override protected void after() { try { for(realmconfiguration configuration : configurations) { realm.deleterealm(configuration); } } catch(illegalstateexception e) { // throw exception caused deleting opened realm if test case doesn't throw. if(!unittestfailed) { throw e; } } { // delete temp folder. super.after(); } } public realmconfiguration createconfiguration() { realmconfiguration configuration = new realmconfiguration.builder(getroot()).build(); configurations.add(configuration); return configuration; } public realmconfiguration createconfiguration(string subdir, string name) { final file folder = new file(getroot(), subdir); asserttrue(folder.mkdirs()); realmconfiguration configuration = new realmconfiguration.builder(folder).name(name).build(); configurations.add(configuration); return configuration; } public realmconfiguration createconfiguration(string name) { realmconfiguration configuration = new realmconfiguration.builder(getroot()).name(name).build(); configurations.add(configuration); return configuration; } public realmconfiguration createconfiguration(string name, byte[] key) { realmconfiguration configuration = new realmconfiguration.builder(getroot()).name(name).encryptionkey(key).build(); configurations.add(configuration); return configuration; } public realmconfiguration.builder createconfigurationbuilder() { return new realmconfiguration.builder(getroot()); } // copies realm file assets temp dir public void copyrealmfromassets(context context, string realmpath, string newname) throws ioexception { // delete existing file before copy realmconfiguration configtodelete = new realmconfiguration.builder(getroot()).name(newname).build(); realm.deleterealm(configtodelete); assetmanager assetmanager = context.getassets(); inputstream = assetmanager.open(realmpath); file file = new file(getroot(), newname); fileoutputstream outputstream = new fileoutputstream(file); byte[] buf = new byte[1024]; int bytesread; while((bytesread = is.read(buf)) > -1) { outputstream.write(buf, 0, bytesread); } outputstream.close(); is.close(); } } /* * copyright 2015 realm inc. * * licensed under apache license, version 2.0 (the "license"); * may not use file except in compliance license. * may obtain copy of license @ * * http://www.apache.org/licenses/license-2.0 * * unless required applicable law or agreed in writing, software * distributed under license distributed on "as is" basis, * without warranties or conditions of kind, either express or implied. * see license specific language governing permissions , * limitations under license. */ package io.realm.rule; import java.lang.annotation.retention; import java.lang.annotation.target; import static java.lang.annotation.elementtype.method; import static java.lang.annotation.retentionpolicy.runtime; /** * annotation should used along {@link runinlooperthread} * when annotation present, test method executed on worker thread looper. * uses {@link org.junit.rules.temporaryfolder} create , open realm. * annotation param {@link io.realm.rule.runinlooperthread.runnablebefore} can supplied run before * looper thread. */ @target(method) @retention(runtime) public @interface runtestinlooperthread { class<? extends runinlooperthread.runnablebefore> value() default runinlooperthread.runnablebefore.class; } /* * copyright 2015 realm inc. * * licensed under apache license, version 2.0 (the "license"); * may not use file except in compliance license. * may obtain copy of license @ * * http://www.apache.org/licenses/license-2.0 * * unless required applicable law or agreed in writing, software * distributed under license distributed on "as is" basis, * without warranties or conditions of kind, either express or implied. * see license specific language governing permissions , * limitations under license. */ package io.realm.rule; import android.os.handler; import android.os.looper; import org.junit.runner.description; import org.junit.runners.model.statement; import java.io.printwriter; import java.io.stringwriter; import java.util.linkedlist; import java.util.uuid; import java.util.concurrent.countdownlatch; import java.util.concurrent.executorservice; import java.util.concurrent.executors; import io.realm.realm; import io.realm.realmconfiguration; import io.realm.testhelper; import static org.junit.assert.fail; /** * rule runs test inside worker looper thread. rule responsible * of creating temp directory containing realm instance delete it, once test finishes. * * realms used in method method annotated {@code @runtestinlooperthread } should use * {@link runinlooperthread#createconfiguration()} , friends create configurations. failing can * result in test failing because realm not deleted (reason {@link testrealmconfigurationfactory} * , class not agree in order delete open realms. */ public class runinlooperthread extends testrealmconfigurationfactory { public realm realm; public realmconfiguration realmconfiguration; private countdownlatch signaltestcompleted; private handler backgroundhandler; // variables created inside test local , eligible gc. // need variables survive across different looper // events (callbacks happening in future), add strong reference // them duration of test. public linkedlist<object> keepstrongreference; @override protected void before() throws throwable { super.before(); realmconfiguration = createconfiguration(uuid.randomuuid().tostring()); signaltestcompleted = new countdownlatch(1); keepstrongreference = new linkedlist<object>(); } @override protected void after() { super.after(); realmconfiguration = null; realm = null; keepstrongreference = null; } @override public statement apply(final statement base, description description) { final runtestinlooperthread annotation = description.getannotation(runtestinlooperthread.class); if(annotation == null) { return base; } return new statement() { private throwable testexception; @override public void evaluate() throws throwable { before(); class<? extends runnablebefore> runnablebefore = annotation.value(); if(!runnablebefore.isinterface()) { runnablebefore.newinstance().run(realmconfiguration); } try { final countdownlatch signalclosedrealm = new countdownlatch(1); final throwable[] threadassertionerror = new throwable[1]; final looper[] backgroundlooper = new looper[1]; final executorservice executorservice = executors.newsinglethreadexecutor(); executorservice.submit(new runnable() { @override public void run() { looper.prepare(); backgroundlooper[0] = looper.mylooper(); backgroundhandler = new handler(backgroundlooper[0]); try { realm = realm.getinstance(realmconfiguration); base.evaluate(); looper.loop(); } catch(throwable e) { threadassertionerror[0] = e; unittestfailed = true; } { try { looperteardown(); } catch(throwable t) { if(threadassertionerror[0] == null) { threadassertionerror[0] = t; } unittestfailed = true; } if(signaltestcompleted.getcount() > 0) { signaltestcompleted.countdown(); } if(realm != null) { realm.close(); } signalclosedrealm.countdown(); } } }); testhelper.exitorthrow(executorservice, signaltestcompleted, signalclosedrealm, backgroundlooper, threadassertionerror); } catch(throwable error) { // these exceptions should come testhelper.awaitorfail() testexception = error; } { // try hard possible close down gracefully, while still keeping exceptions intact. try { after(); } catch(throwable e) { if(testexception != null) { // both testhelper.awaitorfail() , after() threw exception. make sure aware of // fact printing both exceptions. stringwriter teststacktrace = new stringwriter(); testexception.printstacktrace(new printwriter(teststacktrace)); stringwriter afterstacktrace = new stringwriter(); e.printstacktrace(new printwriter(afterstacktrace)); stringbuilder errormessage = new stringbuilder().append("after() threw error shadows test case error") .append('\n') .append("== test case exception ==\n") .append(teststacktrace.tostring()) .append('\n') .append("== after() exception ==\n") .append(afterstacktrace.tostring()); fail(errormessage.tostring()); } else { // after() threw exception throw e; } } // testhelper.awaitorfail() threw exception if(testexception != null) { //noinspection throwfromfinallyblock throw testexception; } } } }; } /** * signal test has completed. */ public void testcomplete() { signaltestcompleted.countdown(); } /** * signal test has completed. * * @param latches additional latches wait before set test completed flag. */ public void testcomplete(countdownlatch... latches) { for(countdownlatch latch : latches) { testhelper.awaitorfail(latch); } signaltestcompleted.countdown(); } /** * posts runnable worker threads looper. */ public void postrunnable(runnable runnable) { backgroundhandler.post(runnable); } /** * tear down logic guaranteed run after looper test has either completed or failed. * run on same thread looper test. */ public void looperteardown() { } /** * if implementation of supplied annotation, {@link runnablebefore#run(realmconfiguration)} * executed before looper thread starts. populating realm before test. */ public interface runnablebefore { void run(realmconfiguration realmconfig); } }
Comments
Post a Comment