When I find something interesting and new, I post it here - that's mostly programming, of course, not everything.

Monday, August 25, 2008

How Do You Test JavaScript Loading?

I had a problem: in my JavaScript unittests I wanted to check the behavior of my scripts when they are reloaded multiple times. That's kind of challenging: scripts are loaded in a background thread, and unittests run in the foreground test.

There seems to be no way to synchronize these two processes, to have either an event listener or a wait function that would make sure assertions in the test cases are called after a certain script is reloaded.

Here is what I was looking for:


function loadScript(path) {
var script = document.createElement("script");
script.src = path;
document.body.appendChild(script);
JS_TOOLS.sleep(500);
}

function loadScripts(var_args) {
for (var i = 0; i < arguments.length; i++) {
loadScript(arguments[i]);
}
}

function testReloading_justOne() {
loadScript('scriptToLoad.js');
assertFalse(A_CERTAIN_VARIABLE_FROM_THE_SCRIPT === undefined);
}


The trick is to put the thread to sleep... for a little while.

I looked around on the internet, and found some cheap and some dear advices. In two words: Use Applet.

So I did. I wrote an applet, looking like this:


public class JsTools extends Applet {

/**
* Sleeps given number of milliseconds.
*
* @param timeToSleep time to sleep (ms)
*/
public void sleep(long timeToSleep) {
for (long wakeupTime = System.currentTimeMillis() + timeToSleep;
timeToSleep > 0;
timeToSleep = wakeupTime - System.currentTimeMillis()) {
try {
Thread.sleep(timeToSleep);
} catch (InterruptedException ie) {
// ignore it, just repeat until done
}
}
}
}



The idea is that I may want to add more functionality to this applet some time later.

Now, I compile this applet, and store it in jsapplet.jar (with the right directory structure, so that it could be found by the browser's jvm. Then I wrote some JavaScript code that loads the applet and exports a function, sleep():


function jstools(location) {
var JSTOOLS_ID = "JSTOOLS_ID";
var userAgent = navigator.userAgent.toLowerCase();
var isIE = userAgent.indexOf('msie') != -1 &&
userAgent.indexOf('opera') < 0;

function _(var_args) {
for (var i = 0; i < arguments.length; i++) {
document.write(arguments[i]);
}
};

function openElement(name, attributes) {
_('<', name);
for (var id in attributes) {
_(' ', id, '="', attributes[id], '"');
}
_('>');
}

function closeElement(name) {
_('');
}

function addParameters(parameters) {
for (var name in parameters) {
openElement('param', {name: name, value: parameters[name]});
}
}

openElement('object',
isIE ?
{
classid: "clsid:8AD9C840-044E-11D1-B3E9-00805F499D93",
style: "border-width:0;",
codebase: "http://java.sun.com/products/plugin/autodl/jinstall-1_4_1-windows-i586.cab#version=1,4,1",
name: JSTOOLS_ID,
id: JSTOOLS_ID
} :
{
type: "application/x-java-applet;version=1.4.1",
name: JSTOOLS_ID,
id: JSTOOLS_ID
});

addParameters({
archive: (location ? (location + '/') : '') + 'jstools.jar',
code: "com.google.javascript.keyboard.JsTools",
mayscript: "yes",
scriptable: "true",
name: "jsapplet"});

closeElement('object');

return {
sleep: function sleep(n) {
document[JSTOOLS_ID].sleep(n);
}
};
};


This is kind of sad that I have to run it before the document ocntent is finalized; there must be a way to use DOM to build the applet element when it is needed - but the straightforward solution does not seem to be working, and what the heck, it is a unittest, where, according to josh kerievsky, everything is allowed.

There's a tricky parameter that one has to pass to tools(): applet location; we may later decide to store it somewhere else, outside our main directory. So, in my unittests, I instantiate my JS_TOOLS variable like this:

var JS_TOOLS = jstools("tools"); // the jar is in tools directory


Well, that's it. The script loads, and the variable instantiates.

Someone would complain about 0.3 second delay in running the test, right? Not clean, not "small", and flaky. Okay, suggest something else if you are really worried about .3 second delay in getting your test results.

P.S. You can try it here.

Tuesday, August 05, 2008

Java Style. Iterating over collections

Suppose you decide to concatenate collections; it should of course look something like this:


Collection<A> cat(Collection<A>... collections);


What you need to do is return an instance of anonymous class extending AbstractCollection<A>; and you will need to implement two methods, iterator() and size().


Collection<A> cat(final Collection<A>... collections) {
return new AbstractCollection<A>() {
public Iterator<A> iterator() {
...
}

public int size() {
...
}
};
}


Implementing size() is almost trivial: we can start with this naive code


public int size() {
int size = 0;
for (Collection<A> c : collections) {
size += c.length;
}
return size;
}


Now raise your hAnds who think this is the correct solution... Wrong.

The problem is that for a large collection the result may be not precise. Quoting Collectio.size javadoc: "Returns the number of elements in this collection. If this collection contains more than Integer.MAX_VALUE elements, returns Integer.MAX_VALUE."

So, let's rewrite size():

public int size() {
int size = 0;
for (Collection<A> c : collections) {
if (c.size() == Integer.MAX_VALUE) return Integer.MAX_VALUE;
size += c.size();
if (size < 0) return Integer.MAX_VALUE;
}

return size;
}


If you find an error in the code above, let me know.

Now, iterator(). Not that I doubt you can implement it; what I want to say is that in this exercise minute details turn out to be important.

Here's our first attempt:


Collection<A> cat(final Collection<A>... collections) {
return new AbstractCollection<A>() {
public Iterator<A> iterator() {
private int i = -1;
private Iterator<A> current = null;


public boolean hasNext() {
while (i < collections.size()) {
if (current != null && current.hasNext()) return true;
current = collections[++i].next().iterator();
}
return false;
}

public A next() {
if (hasNext()) return current.next();
throw new NoSuchElementException();
}

public void remove() {
if (current != null) current.remove();
}
}
...
}
}




Here we are trying to be smart, bypassing empty collections, and getting to the one that has something; this will also work if the argument list is empty.

The problem is, it won't work if the argument list is not empty: see we are doing this assignment,

current = collections[++i].next().iterator();
- when i reaches collections.length - 1, we will go overboard. So we have to fix this, replacing the line with

if (i < collections.length) current = collections[++i].next().iterator();


This does look disgusting! In addition to checking for null, we are checking the same condition twice!

Null... can we get rid of null? Let's try this: set the initial value of current to an empty iterator:

Iterator<A> current = Collections.EMPTY_SET.iterator();


So that now we can say just

if (current.hasNext()) return true;

instead of

if (current != null && current.hasNext()) return true;


Good, good. How about index checking?

I have this idea: replace an array with a list (actually, an iterator). We take the array of arguments and turn it into a list, like this:

public Iterator<A> iterator() {
return new Iterator<A>() {
// Top iterator
private Iterator<Collection<A>> i = Arrays.asList(collections).iterator();
// Bottom iterator
private Iterator<A> j = Collections.EMPTY_SET.iterator();

public boolean hasNext() {
for (;!j.hasNext() && i.hasNext(); j = i.next().iterator());
return j.hasNext();
}

public A next() {
if (hasNext()) return j.next();
throw new NoSuchElementException();
}

public void remove() {
j.remove();
}
};
}


Amazing, heh? Where's double index checking? It's gone. It's all hidden inside the delegate iterator.
That's final.

Followers

Subscribe To My Podcast

whos.amung.us