Saturday, March 26, 2011

Your test cases should sometimes fail!

I'm an avid subscriber of the delightful weekly (sometimes) Python-URL! email, highlighting the past week's interesting discussions across the numerous Python lists. Each summary starts with the best quote from the week; here's last week's quote:
"So far as I know, that actually just means that the test suite is insufficient." - Peter Seebach, when an application passes all its tests.
I wholeheartedly agree: if your build always passes its tests, that means your tests are not tough enough! Ideally the tests should stay ahead of the software, constantly pulling you forwards to improve its quality. If the tests keep passing, write new ones that fail! Or make existing ones evil-er.

You'll be glad to know that Lucene/Solr's tests do sometimes fail, as you can see in the Hudson Jenkins automated trunk builds.


Randomized testing

Our test infrastructure has gotten much better, just over the past 6 months or so, through heavy use of randomization.

When a test needs a Directory instance, but doesn't care which, it uses the newDirectory method. This method picks one of Lucene's Directory implementations (RAMDirectory, NIOFSDirectory, MMapDirectory, etc.) and then wraps it with MockDirectoryWrapper, a nice little class that does all sorts of fun things like: occasionally calling Thread.yield; preventing still-open files from being overwritten or deleted (acts-like-Windows); refusing to write to the same file twice (verifying Lucene is in fact write-once); breaking up a single writeBytes into multiple calls; optionally throwing IOException on disk full, or simply throwing exceptions at random times; simulating an OS/hardware crash by randomly corrupting un-sync'd files in devilish ways; etc. We pick a timezone and locale.

To randomize indexing, we create a IndexWriterConfig, tweaking all sorts of settings, and use RandomIndexWriter (like IndexWriter, except it sometimes optimizes, commits, yields, etc.). The newField method enables or disables stored fields and term vectors. We create random codecs, per field, by combining a terms dictionary with a random terms index and postings implementations. MockAnalyzer injects payloads into its tokens.

Sometimes we use the PreFlex codec, to writes all indices in the 3.x format (so that we test index backwards compatibility), and sometimes the nifty SimpleText codec. We have exotic methods for creating random yet somewhat realistic full Unicode strings. When creating an IndexSearcher, we might use threads (pass an ExecutorService), or not. We catch tests that leave threads running, or that cause insanity in the FieldCache (for example by loading both parent and sub readers).


Reproducibility

To ensure a failure is reproducible, we save the random seeds and on a failure print out a nice line like this:
NOTE: reproduce with: ant test -Dtestcase=TestFieldCacheTermsFilter -Dtestmethod=testMissingTerms -Dtests.seed=-1046382732738729184:5855929314778232889
This fixes the seed so that the test runs deterministically. Sometimes, horribly, we have bugs in this seed logic, thus causing tests to not run deterministically and we scramble to fix those bugs first!

If you happen to hit a test failure, please send that precious line to the dev list! This is like the Search for Extraterrestrial Intelligence (SETI): there are some number of random seeds out there (hopefully, not too many!), that will lead to a failure, and if your computer is lucky enough to discover one of these golden seeds, please share the discovery!

The merging of Lucene and Solr's development was also a big step forward for test coverage, since every change in Lucene is now tested against all of Solr's test cases as well.

Tests accept a multiplier to crank things up, causing them to use more test documents or iterations, run for longer time, etc. We now have perpetual jobs on Jenkins, for both 3.x and trunk, launching every 15 minutes with multiplier 5. We know quickly when someone breaks the build!

This added test coverage has already caught a number of sneaky bugs (including a rare index corruption case on disk-full and a chunking bug in MMapDirectory) that we otherwise would not have discovered for some time.

The test infrastructure itself is so useful that it's now been factored out as a standalone JAR so apps using Lucene can tap into it to create their own fun randomized tests.

4 comments:

  1. Hi,

    My Question is related to OpenNLP Integration with SOLR.
    I have successfully applied OpenNLP LUCENE-2899-x.patch to latest solr branch checkout from here:
    http://svn.apache.org/repos/asf/lucene/dev/branches/branch_4x
    And also iam able to compile source code, generated all realted binaries and able to create war file.
    But facing issues while deployment of SOLR.
    Here is the error
    Caused by: org.apache.solr.common.SolrException: Plugin init failure for [schema.xml] fieldType "text_opennlp": Plugin init failure for [schema.xml] a
    nalyzer/tokenizer: Error loading class 'solr.OpenNLPTokenizerFactory'
    at org.apache.solr.util.plugin.AbstractPluginLoader.load(AbstractPluginLoader.java:177)
    at org.apache.solr.schema.IndexSchema.readSchema(IndexSchema.java:467)
    ... 15 more
    Caused by: org.apache.solr.common.SolrException: Plugin init failure for [schema.xml] analyzer/tokenizer: Error loading class 'solr.OpenNLPTokenizerFa
    ctory'
    at org.apache.solr.util.plugin.AbstractPluginLoader.load(AbstractPluginLoader.java:177)
    at org.apache.solr.schema.FieldTypePluginLoader.readAnalyzer(FieldTypePluginLoader.java:362)
    at org.apache.solr.schema.FieldTypePluginLoader.create(FieldTypePluginLoader.java:95)
    at org.apache.solr.schema.FieldTypePluginLoader.create(FieldTypePluginLoader.java:43)
    at org.apache.solr.util.plugin.AbstractPluginLoader.load(AbstractPluginLoader.java:151)
    ... 16 more
    Caused by: org.apache.solr.common.SolrException: Error loading class 'solr.OpenNLPTokenizerFactory'
    at org.apache.solr.core.SolrResourceLoader.findClass(SolrResourceLoader.java:449)
    at org.apache.solr.core.SolrResourceLoader.newInstance(SolrResourceLoader.java:543)
    at org.apache.solr.schema.FieldTypePluginLoader$2.create(FieldTypePluginLoader.java:342)
    at org.apache.solr.schema.FieldTypePluginLoader$2.create(FieldTypePluginLoader.java:335)
    at org.apache.solr.util.plugin.AbstractPluginLoader.load(AbstractPluginLoader.java:151)
    ... 20 more
    Caused by: java.lang.ClassNotFoundException: solr.OpenNLPTokenizerFactory
    at java.net.URLClassLoader$1.run(URLClassLoader.java:366)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:423)
    at java.net.FactoryURLClassLoader.loadClass(URLClassLoader.java:789)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:356)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:264)
    at org.apache.solr.core.SolrResourceLoader.findClass(SolrResourceLoader.java:433)
    ... 24 more
    4446 [coreLoadExecutor-3-thread-1] ERROR org.apache.solr.core.CoreContainer û null:org.apache.solr.common.SolrException: Unable to create core: colle
    ction1
    at org.apache.solr.core.CoreContainer.recordAndThrow(CoreContainer.java:931)
    at org.apache.solr.core.CoreContainer.create(CoreContainer.java:563)
    at org.apache.solr.core.CoreContainer$1.call(CoreContainer.java:244)
    at org.apache.solr.core.CoreContainer$1.call(CoreContainer.java:236)
    at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:334)
    at java.util.concurrent.FutureTask.run(FutureTask.java:166)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)
    at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:334)
    at java.util.concurrent.FutureTask.run(FutureTask.java:166)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)
    at java.lang.Thread.run(Thread.java:722)
    Please help me on this.

    Waiting for your reply.
    Thanks in advance.

    ReplyDelete
  2. Hi, could you ask this on the solr-user@lucene.apache.org list?

    ReplyDelete