Hi Ryan,

So. I discussed this a while ago, but here it comes again. Let me
first clear a few things from what you said.

> Only in the last month or so did I learn that -Dtests.iters doesn't really 
> "work".  What I mean is in regards to randomization.

This is not true. It works (as I will explain below). Try it, for
example (the annotation has the same effect as providing
-Dtests.iters=10):

@Repeat(iterations = 10)
@Seed("0")
public class Test001 extends RandomizedTest {
  @Test public void test() {
    System.out.println(randomAsciiOfLength(10));
  }
}

I fixed the initial seed to make it reproducible. This will print:

nKtjLXhWQw
awHTHLIGAq
vEYgnxTkWv
mSAloRXtIV
iBhCJuZNzP
DHAIyqecSS
zaEoTAWAOa
CoraUrKuib
fKxUZnyQTx
beFtvsUTHc

> Each iteration currently is *exactly* the same as far as randomization
>> (each iteration uses the same master seed).

You can see above that it isn't true. Every iteration is different and
uses different randomness (and this randomness is "derived" from the
(master, iteration) pair so it is fully reproducible in each run).

>> Why not create a different seed for each iteration when -Dtests.iters is 
>> used?

Let's talk about JUnit unit tests and how (any) runner should execute
them. I will demonstrate this on a simple class like this one (pseudo
code):

class Foo {
  @BeforeClass beforeClassHook() {}

  @Before beforeHook() {}
  @Test test1() {}
  @After afterHook() {}

  @AfterClass afterClassHook() {}
}

There are a couple of "stages" to be executed. Simplifying a bit, it
looks like this.

0. Prerequsite

- class available, possible loaded and initialized

1. Setup:

- extract test methods

2. Execution.

- run class-before hooks (rules, @BeforeClass)
- for each test:
    run before hooks (@Before, rules)
    run the test itself
    run after hooks (@After, rules)
- run class-after hooks (rules, @AfterClass)

For the class above, the sequence of method calls would be:

beforeClassHook()

new() // constructor
beforeHook()
test1()
afterHook()

afterClassHook()

If you were to multiply tests execution manually, you would copy-paste
the test method giving it a different name:

class Foo {
  @BeforeClass beforeClassHook() {}

  @Before beforeHook() {}
  @Test test1() {}
  @Test test2() {}
  @After afterHook() {}

  @AfterClass afterClassHook() {}
}

which would result in a sequence of calls like this one:

beforeClassHook()

new() // constructor
beforeHook()
test1()
afterHook()

new() // constructor (new instance)
beforeHook()
test2()
afterHook()

afterClassHook()

So, first of all, note that duplicating tests is *not* equivalent to
just looping around method body. Each execution should be run on a new
instance and wrapped with setup and teardown hooks, otherwise it's not
really an isolated JUnit test anymore (and it would be against JUnit
informal execution flow).

This is, in short, what -Dtests.iters does (and what @Repeat does) --
it replicates every test, making sure they have unique names (IDEs get
confused if they don't) and trying to work around other issues I won't
discuss here. It does work. The reason you believe it doesn't work is
because most of the stuff in LuceneTestCase is initialized at *static*
class level, which by definition is executed only once, regardless of
the number of tests in a class. Let's modify our initial example a
bit:

@Repeat(iterations = 10)
@Seed("0")
public class Test002 extends RandomizedTest {
  static String s;

  @BeforeClass
  public static void beforeClass() {
    s = randomAsciiOfLength(10);
  }

  @Test public void test() {
    System.out.println(s);
  }
}

If you run this, you'll see:

SXVNjhPdQD
SXVNjhPdQD
SXVNjhPdQD
SXVNjhPdQD
SXVNjhPdQD
SXVNjhPdQD
SXVNjhPdQD
SXVNjhPdQD
SXVNjhPdQD
SXVNjhPdQD

This works as expected because beforeClass() is invoked once (even if
every test has a different randomness available to it). LuceneTestCase
does it for performance reasons (that static initialization is fairly
costly). This ends the
JUnit part of the story.

But wait, there is more. If you take a look above at how JUnit runners
should work they load the class (or in fact are given an initialized
class) before they can do anything. So if there are static class
initializers (static { field = foo(); }) then these may get executed
well before the runner has any chance to initialize its own stuff --
that's why you *have* to use @BeforeClass methods if you want to use
RandomizedTest's randomness; doing
this:

@Repeat(iterations = 10)
@Seed("0")
public class Test003 extends RandomizedTest {
  static final String s;
  static {
    s = randomAsciiOfLength(10);
  }

  @Test public void test() {
    System.out.println(s);
  }
}

will result in an initialization exception complaining about missing
random context:

java.lang.IllegalStateException: No context information for thread:
Thread[id=11, name=SUITE-Test003-seed#[0], state=RUNNABLE,
group=TGRP-Test003]. Is this thread running under a class
com.carrotsearch.randomizedtesting.RandomizedRunner runner context?
Add @RunWith(class
com.carrotsearch.randomizedtesting.RandomizedRunner.class) to your
test class. Make sure your code accesses random contexts within
@BeforeClass and @AfterClass boundary (for example, static test class
initializers are not permitted to access random contexts).

Finally, all the above probably begs the question why can't the runner
just wrap every iteration one level up the ladder -- re-execute static
hooks, etc.

The answer is that you are only considering the level of a single
class and the master seed is in fact used for other things, most
notably for ordering test classes for predictable execution within one
forked JVM (this is done in JUnit4-Ant task). If you re-executed one
class's static hooks with a different seed for different iterations
how would they report the "real master" seed back for re-running the
entire suite? Should it be a pair master-seed:iteration? Only the derived
class seed? Or maybe we should have two seeds -- one for ordering classes
and one for the actual class execution?

It gets really messy if you get down to details like this, not
to mention missing support from IDEs for running multiple tests of the
same class (it's already pretty complicated to make it work in
Eclipse, IntelliJ Idea, etc. when multiple identical methods are executed).

Finally:

>> different people have their own "beasting" scripts
>> that run the test essentially N times from a shell to force different
>> seeds in each iteration.

This is true. ButI don't think Mike, for example, will resign from
his scripts even if "real" class-level tests.iters would be
implemented -- Mike's
script runs tests across multiple machines via SSH; I won't have the
time for such  distributed extensions in any near future.

If you want to try to implement reiteration at the 'static context'
feel free to give it a shot though; I would certainly be interested in
how you approach the problems I mentioned above. A good place to start
would be to modify JUnit4-ant (Ant task), around here:

https://github.com/carrotsearch/randomizedtesting/blob/master/junit4-ant/src/main/java/com/carrotsearch/ant/tasks/junit4/JUnit4.java#L887

if you somehow associated a class with its master seed (and duplicated
classes as well), then you could fork off multiple class executions
with different seed (you'd still have to modify the forked subprocess
to accept class name and master seed; currently only one master seed
is passed at the start of each JVM.

Or you could do what I mentioned above -- separate the seed used for
class ordering from the one used for executing
every class (every iteration of a class).

Or maybe it'd be easier to modify the runner itself (then duplication
would work from IDE level)... but then you'll hit the IDE issues and
constraints... I really don't know which way is better (or worse).
That's part of the challenge I guess. :)

I'll try to look at the code again in the next few days and will give
you some feedback. I can't log to Jira for some reason, but I'll
lookup the issue number for this, it's been there for a good while.

Dawid

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@lucene.apache.org
For additional commands, e-mail: dev-h...@lucene.apache.org

Reply via email to