Minborg

Minborg
Minborg
Showing posts with label permutations. Show all posts
Showing posts with label permutations. Show all posts

Tuesday, June 7, 2022

Exhaustive JUnit5 Testing with Combinations, Permutations and Products

 

Exhaustive JUnit5 Testing with Combinations, Permutations, and Products

Unit testing constitutes an integral part of the process of providing high-quality software. But, how can one write tests covering all variants of several operations? Read this article and learn how to use JUnit5 in conjunction with combinations, permutations, and products.

Test Support Libraries

There are many libraries that make testing better in different aspects. Here are some of them:

Agitar One

Agitator automatically creates dynamic test cases, synthesizes sets of input data, and analyzes the results.


http://www.agitar.com/solutions/products/software_agitation.html


Chaos Monkey

Chaos Monkey is responsible for randomly terminating instances in production to ensure that engineers implement their services to be resilient to instance failures.


https://netflix.github.io/chaosmonkey/


Chronicle Test Framework

This library provides support classes for writing JUnit tests. It supports both JUnit4 and JUnit5. Tools like permutations, combinations, and products can be leveraged to create exhaustive tests. Agitar One can automatically identify and generate tests whereas Chronicle Test Framework provides programmatic support for writing your own tests.

 

https://github.com/OpenHFT/Chronicle-Test-Framework


In this article we will be talking about Chronicle Test Framework.

Objective

Suppose we have written a custom implementation of java.util.List called MyList and that we want to test the implementation against a reference implementation such as ArrayList. Further, assume that we have a number of operations  O1, O2, …, On. that we can apply to these objects. The question now is: how can we write tests assuring that the two objects provide the same result regardless of how the operations are applied?

Combinations, Permutations, and Products

Before we continue, we remind ourselves of what combinations, permutations, and products are from a mathematical perspective. Assume we have a set X = {A, B, C} then;


The combinations of X are:

[]

[A]

[B]

[C]

[A, B]

[A, C]

[B, C]

[A, B, C]


The permutations of X are:

[A, B, C]

[A, C, B]

[B, A, C]

[B, C, A]

[C, A, B]

[C, B, A]


The permutations of all combinations of X are:

[]

[A]

[B]

[C]

[A, B]

[B, A]

[A, C]

[C, A]

[B, C]

[C, B]

[A, B, C]

[A, C, B]

[B, A, C]

[B, C, A]

[C, A, B]

[C, B, A]


Where [] denotes a sequence of no elements. As can be seen from the sequences above, the number of variants will increase very rapidly as the number of set members increases. This puts a practical limit on the exhaustiveness of the test one can write.


Another concept (not exemplified in this article) is “Products” (aka Cartesian Products). Suppose we have a sequence s1 = [A, B] of one type and another sequence s2 = [1, 2] of another type then;


The product of s1 and s2 is:

[A, 1]

[A, 2]

[B, 1]

[B, 2]


Products have many similarities with database “join” operations and relate to nested loops.

The Test Framework

In this test, we will be using open-source Chronicle-Test-Framework that supports the aforementioned combination, permutation, and product features. Here is an example:


// Prints: [], [A], [B], [C], [A, B], [B, C], … (8 sequences)

Combination.of("A", "B", "C")

    .forEach(System.out::println)


This prints all the combinations of {A, B, C} the result being the same as in the previous chapter. In the same way, the following example will print out all the permutations of {A, B, C}:


// Prints: [A, B, C], [A, C, B], [B, A, C], … (6 sequences)

Permutation.of("A", "B", "C")

    .forEach(System.out::println)


In this following example, we combine the capabilities of combinations and permutations:


// Prints: [], [A], [B], [C], [A, B], [B, A], …(16 sequences)

Combination.of("A", "B", "C")

    .flatMap(Permutation::of)

    .forEach(System.out::println)


The methods above produce a Stream of Collection elements allowing easy adaptations to other frameworks such as JUnit5.


When it comes to products, things work slightly differently. Here is an example:


final List<String> strings = Arrays.asList("A", "B");

final List<Integer> integers = Arrays.asList(1, 2);


Product.of(strings, integers)

        .forEach(System.out::println);


This will print:


Product2Impl{first=A, second=1}

Product2Impl{first=A, second=2}

Product2Impl{first=B, second=1}

Product2Impl{first=B, second=2}


If a more recent Java version is used, the following scheme would typically be used instead:


record StringInteger(String string, Integer integer){}


Product.of(strings, integers, StringInteger::new)

        .forEach(System.out::println);


This will produce:


StringInteger[string=A, integer=1]

StringInteger[string=A, integer=2]

StringInteger[string=B, integer=1]

StringInteger[string=B, integer=2]


In my opinion, the above is better than the default Product2Impl tuple because the record is a “nominal tuple” where the names and types of the state elements are declared in the record header compared to the Product2Impl which relies on generic types and “first” and “second” as names.

The Objective

Suppose that we want to make sure two java.lang.List implementations behave the same way for a number of mutating operations.  Here, lists contain Integer values (ie implements List<Integer>).


We start by identifying the mutating operations to use. One (arbitrary) suggestion is to use the following operations:


list.clear()

list.add(1)
list.remove((Integer) 1) // Will remove the object 1 and not @index 1

list.addAll(Arrays.asList(2, 3, 4, 5))

list.removeIf(ODD)


Where Predicate<Integer> ODD = v -> v % 2 == 1.


In this article, we will initially use ArrayList and LinkedList for comparison.


As the reader might suspect, the List types are going to be tested against each other with all the possible sequences of operations. But, how can this be achieved?

The Solution

JUnit5 provides a @TestFactory annotation and DynamicTest objects, allowing a test factory to return a Stream of DynamicTest instances. This is something that we can leverage, as shown below:


 private static final Collection<NamedConsumer<List<Integer>>> OPERATIONS =
    Arrays.asList(

        NamedConsumer.of(List::clear, "clear()"),

        NamedConsumer.of(list -> list.add(1), "add(1)"),

        NamedConsumer.of(list -> list.remove((Integer) 1), "remove(1)"),

        NamedConsumer.of(list -> list.addAll(Arrays.asList(2, 3, 4, 5)),
                                "addAll(2,3,4,5)"),

        NamedConsumer.of(list -> list.removeIf(ODD), "removeIf(ODD)")

);



@TestFactory

Stream<DynamicTest> validate() {

    return DynamicTest.stream(Combination.of(OPERATIONS)

                    .flatMap(Permutation::of),

            Object::toString,

            operations -> {

                List<Integer> first = new ArrayList<>();

                List<Integer> second = new LinkedList<>();

                operations.forEach(op -> {

                    op.accept(first);

                    op.accept(second);

                });

                assertEquals(first, second);

            });

}


This is the entire solution, and when run under IntelliJ (or other similar tools), the following tests will be performed (only the first 16 of the 326 tests are shown for brevity):



Image 1, Shows the first 16 test runs of the 326 tests.


The reason NamedConumer is used over the standard java.util.function.Consumer is that it allows real names to be shown rather than some cryptic lambda reference like ListDemoTest$$Lambda$350/0x0000000800ca9a88@3d8314f0. It certainly looks better in the output and provides much better debug capabilities.

Expanding the Concept

Suppose we have a more significant number of List implementations we’d like to test, such as:


ArrayList

LinkedList

CopyOnWriteArrayList

Stack

Vector


To expand the concept, we begin by creating a collection of List<Integer> constructors:


private static final Collection<Supplier<List<Integer>>> CONSTRUCTORS =
        Arrays.asList(

            ArrayList::new,

            LinkedList::new,

            CopyOnWriteArrayList::new,

            Stack::new,

            Vector::new);


The reason for working with constructors rather than instances is that we need to be able to create new List implementations for each dynamic test.


Now, we only need to do minor modifications to the previous test:


@TestFactory

Stream<DynamicTest> validateMany() {

    return DynamicTest.stream(Combination.of(OPERATIONS)

                    .flatMap(Permutation::of),

            Object::toString,

            operations -> {


                // Create a fresh list of List implementations

                List<List<Integer>> lists = CONSTRUCTORS.stream()

                        .map(Supplier::get)

                        .collect(Collectors.toList());


                // For each operation, apply the operation on each list

                operations.forEach(lists::forEach);


                // Test the lists pairwise

                Combination.of(lists)


                    // Filter out only combinations with two lists

                    .filter(set -> set.size() == 2)


                    // Convert the Set to a List for easy access below

                    .map(ArrayList::new)


                    // Assert the pair equals

                    .forEach(pair -> assertEquals(pair.get(0), pair.get(1)))

            });

}


Strictly, this is a bit of cheating as a single dynamic test contains several subtests for a plurality of List pairs. However, It would be reasonably easy to modify the code above to flatMap() in the various assertions under separate dynamic tests. This is left to the reader as an exercise.

A Final Warning

Combinations, permutations, and products are potent tools that can increase test coverage, but they can also increase the time it takes to run all tests as the number of sequences explodes even with only a moderate increase in the number of input variants. 


For example, here is a list of the number of permutations of all combinations for some given sets S of size s:

 

s = size(S)

poc(s) (permutations of combinations)

0

1 

1

2

2

5

3

16

4

65

5

326

6

1,957

7

13,700

8

109,601

9

986,410

10

986,4101

11

108,505,112


Table 1, shows the number of permutations of combinations for some set sizes.


More generally, it can be shown that: poc(s) = poc(s - 1) * s + 1. 


If in doubt, the number of combinations can be checked using the .count() stream operation which will return the number of sequences returned as shown below:


long poc6 = Combination.of(1, 2, 3, 4, 5, 6)

        .flatMap(Permutation::of)

        .count();


The stream above will, unsurprisingly, return 1,957 as there are six input elements.


Combinatorics can make a huge impact. However, make sure to keep the number of dynamic tests under control, or else test time might increase significantly.


Libraries like Chronicle Queue and Chronicle Map make use of the Chronicle-Test-Framework to improve testing. Here is an example from Chronicle Map where five Map operations are exhaustively tested providing 326 dynamic tests in just a few lines of code.



Resources

Open-Source Chronicle-Test-Framework

The code in this article (Open-Source)

Friday, July 3, 2015

Java 8, Master Permutations

Background

Every morning when you wake up, you need to perform a set of tasks before you go to work or do something else you'd like. Have you ever thought of each and every possible order you can start your day in?

Using Permutations, you can try all combinations of an input set. As you will learn in this post, there is an astonishing number of combinations even for a moderately numbered set. For example, if you have just ten items, you can combine them in over three million ways.

In this post, we will build a small Java 8 support class named Permutations that can be used in unit testing, games strategy evaluation, problem solving and in many other powerful applications.

The Mathematics

You do not have to read this chapter in order to understand this post. However, it helps. So, try to stay on as long as you can before skipping to the next chapter.

The term permutation relates to the process of arranging all the members of a set in an order or, if the set is already ordered, rearranging (or mathematically speaking permutating) the order of the set. For example, if we have a set {1, 2, 3} we can arrange that set in six different ways; {1, 2, 3}, {1, 3, 2}, {2, 1, 3}, {2, 3, 1}, {3, 1, 2}, {3, 2, 1}.

The number of permutations for n distinct objects in a set is said to be n-factorial, mathematically written as n!. The value of n! can be calculated as the factor of one multiplied by all positive integers less than or equal to n. So, if we take our previous set {1, 2, 3}, we will see that it has three distinct members. Thus, there will be 3! = 1*1*2*3 = 6 unique combinations. Encouragingly enough, this is consistent with the example above, where we listed all the six combinations. The observant reader now concludes that one may ignore the serial factor of one since one is the multiplicative identity number, i.e. for any number a, it is true that 1*a = a. In pure English, this means that you can skip multiplying with 1 since you will get back the same result anyhow. So, n! is really 1 multiplied with 2*3*....*n.

Now, if we produce a list of the number of distinct members in a set and calculate the corresponding n-factorial, we will arrive at the following table:

0!   1 (by definition, 1 without multiplying with anything)
1!   1 (obviously you can only arrange a single item in one way)
2!   2
3!   6 (as in the example above)
4!   24
5!   120 (the largest n-factorial value that can fit in a byte (max 2^7 = 127))
6!   720
7!   5040 (the largest n-factorial value that can fit in a short (max 2^15 = 32768))
8!   40320 
9!   362880
10!   3628800
11!   39916800
12! 479001600 (the largest n-factorial value that can fit in an int (max 2^31  = 2147483647))
13! 6227020800
14! 87178291200
15! 1307674368000
16! 20922789888000
17! 355687428096000
18! 6402373705728000
19! 121645100408832000
20! 2432902008176640000 (the largest n-factorial value that can fit in a long (max 2^63))
21! 51090942171709440000
...
42! 1405006117752879898543142606244511569936384000000000 (~number of atoms on earth)

As  can be seen, you have to take care when applying permutations in your code, because you might experience extremely long execution times even for a small number of items. If you, for example, can evaluate any given combination in 1 ms and you have 16 items, the wall clock time for a single thread will be 16!/1000 s > 600 years!

The Permutations class

There are many ways to implement a permutator in a programming language. In this post, my objective was to present the shortest possible code for a permutator in Java 8, so if you can come up with a shorter one, please leave a comment below. Thus, I have opted for simplicity rather than performance.

Secondly, I wanted to work with Java 8 Streams, allowing integration with the powerful Stream functions that are available.

Implementation

The first method we need is the n-factorial function that calculates the number of unique combination of a distinct set. I have written a post on this before and you can read the details here. This is how it looks like:

public long factorial(int n) {
    if (n > 20 || n < 0) throw new IllegalArgumentException(n + " is out of range");
    return LongStream.rangeClosed(2, n).reduce(1, (a, b) -> a * b);
}
Note that the function will only handle an input range of  0  <= n <= 20 because 0 is the lowest defined input value for n-factorial and 20 corresponds to the maximum return value a long can hold.

Next thing we need is a method that returns a numbered permutation of any given input List. So if we, for example, have a List with two items "A" and "B", the first permutation (0) is {A, B} and the second permutation (1) is {B, A}. If we have a List with tree items {A, B, C}, then we have the following permutations:

no   Permutation
--   ---------------
0 {A, B, C}
1 {A, C, B}
2 {B, A, C}
3 {B, C, A}
4 {C, A, B}
5 {C, B, A}

We need to make yet another table of a List before the pattern that I am going to exploit becomes obvious. The complete permutations for the List {A, B, C, D} looks like this:

no   Permutation
--   ---------------
0 {A, B, C, D}
1 {A, B, D, C}
2 {A, C, B, D}
3 {A, C, D, B}
4 {A, D, B, C}
5 {A, D, C, B}
6 {B, A, C, D}
7 {B, A, D, C}
8 {B, C, A, D}
9 {B, C, D, A}
10 {B, D, A, C}
11 {B, D, C, A}
12 {C, A, B, D}
13 {C, A, D, B}
14 {C, B, A, D}
15 {C, B, D, A} (Used in an example below)
16 {C, D, A, B}
17 {C, D, B, A}
18 {D, A, B, C}
19 {D, A, C, B}
20 {D, B, A, C}
21 {D, B, C, A}
22 {D, C, A, B}
23 {D, C, B, A}

Take a look at the first item in each row and you will see that it is changing in sequence of the input list. Let n be the number of items in the input List (n = 4) and subFactorial be (n-1)! (subFactorial = (4-1)! = 3!  = 6) then the first item in the list for permutation no is at index no/subFactorial. In the example above, the first item for permutation 15 is at index no/subFactorial = 15/6 = 2, which corresponds to "C" (remember that "A" is at index 0, "B" is at index 1 and so forth).

This means that we, for any input List, can reduce the problem by picking the first item according to the algorithm above and then recursively invoke the same method again, but now with a reduced number of items in the input list. A classical divide-and-conquer approach. This is how it looks like:

private static <T> List<T> permutationHelper(long no, LinkedList<T> in, List<T> out) {
    if (in.isEmpty()) return out;
    long subFactorial = factorial(in.size() - 1);
    out.add(in.remove((int) (no / subFactorial)));
    return permutationHelper(no % subFactorial, in, out);
}
This helper method takes the permutation number no, an input List in with the initial ordered items and then an out List used to build up the result as the recursion progresses.

As common recursion practice dictates, we start with the base case where we handle the trivial case where recursion shall stop. If the in List is empty, then we are ready and just return the now completed out list.

If the in List was not empty, then we first calculate the previously mentioned subFactorial which is (in.size()-1)! as explained above. This value is then used later on in the method.

The next line is really all logic in the method. We first calculate the index of the item we want to put first in the out List. The index of the item is (int)(no/subFactorial) and we remove this from the in List. As the remove method conveniently returns the item we just removed, we can use remove's return value directly and just add it to the end of the out List in one step.

Now that we have reduced the problem from n to n-1 we need to recurse in a proper way. We call our permutationHelper() method with the same in and out lists we got as input parameters but the permutaion number no is now the remainder of the previous division. This is logical because we handled only the corresponding whole part of the division before. So, we invoke with no%subFactorial which is the remainder that remains to handle.

This is really all there is to do. Those five innocent lines can execute in under 1 ms or perhaps in over 2000 years depending on your input.

The helper method is just there to help us write the real permutator that we are exposing in the class. Here is the exposed wrapper:

public static <T> List<T> permutation(long no, List<T> items) {
    return permutationHelper(
        no,
        new LinkedList<>(Objects.requireNonNull(items)),
        new ArrayList<>()
    );
}
We provide the permutation number no and the List we want to use as input for our method and we just create the in and out List and call the helper method. Simple.

So now we can compute any permutation of a given List. But the Stream support remains, the one  that can give us streams of permutated lists. Providing such a method is really trivial now that we got our permutation() method. This is how it can be done:

public static <T> Stream<Stream<T>> of(T... items) {
    List<T> itemList = Arrays.asList(items);
    return LongStream.range(0, factorial(items.length))
            .mapToObj(no -> permutation(no, itemList).stream());
}

All the hard work is done by the LongStream which supports laziness, parallelism, etc for free and we only map the long Stream to what we would like to have. The long stream provides the input longs 0, 1, 2, ..., (items.lenght)! and then we map those longs to the corresponding permutation. It is really simple.

Note that I have opted to return a Stream of Stream rather than a Stream of List or something similar. If you want a Stream of List, you just change the return type and the mapping.

Now that we have completed everything we need, I will provide the entire Permutations class before we start putting it to use with some examples:

public class Permutations {
    
    private Permutations() {
    }

    public static long factorial(int n) {
        if (n > 20 || n < 0) throw new IllegalArgumentException(n + " is out of range");
        return LongStream.rangeClosed(2, n).reduce(1, (a, b) -> a * b);
    }

    public static <T> List<T> permutation(long no, List<T> items) {
        return permutationHelper(no,
              new LinkedList<>(Objects.requireNonNull(items)),
              new ArrayList<>());
    }

    private static <T> List<T> permutationHelper(long no, LinkedList<T> in, List<T> out) {
        if (in.isEmpty()) return out;
        long subFactorial = factorial(in.size() - 1);
        out.add(in.remove((int) (no / subFactorial)));
        return permutationHelper((int) (no % subFactorial), in, out);
    }

    @SafeVarargs
    @SuppressWarnings("varargs") // Creating a List from an array is safe
    public static <T> Stream<Stream<T>> of(T... items) {
        List<T> itemList = Arrays.asList(items);
        return LongStream.range(0, factorial(items.length))
                .mapToObj(no -> permutation(no, itemList).stream());
    }

}

Examples


We start with testing our factorial() method a bit with the following code snippet:
List<String> items = Arrays.asList("A", "B", "C");
long permutations = Permutations.factorial(items.size());
System.out.println(items + " can be combined in " + permutations + " different ways:");
which will print out:
[A, B, C] can be combined in 6 different ways:
This looks encouraging. We know from the previous chapter that one can combine three distinct objects in six ways. So now we want to see facts on the table: What are those combinations? One way of seeing them all is to use our permutation() method. If we add the following code to our existing snippet:
LongStream.range(0, permutations).forEachOrdered(i -> {
        System.out.println(i + ": " + Permutations.permutation(i, items));
    });
We will get the following output:
[A, B, C] can be combined in 6 different ways:
0: [A, B, C]
1: [A, C, B]
2: [B, A, C]
3: [B, C, A]
4: [C, A, B]
5: [C, B, A]
Wow! It works. It looks exactly the same as in the table in the previous chapter. Now let's take the Permutaions.of() method for a spin like this:
Permutations.of("A", "B", "C")
        .map(s -> s.collect(toList()))
        .forEachOrdered(System.out::println);
The Permutations.of() method will provide all permutations of {A, B, C} and then we will collect those permutations into lists and subsequently print the lists like this:
[A, B, C]
[A, C, B]
[B, A, C]
[B, C, A]
[C, A, B]
[C, B, A]
Sometimes we are interested in just generating a single sequence in all possible orders and this can also be done easily in a similar way like this:
Permutations.of("A", "B", "C")
        .flatMap(Function.identity())
        .forEachOrdered(System.out::print);
Which will produce:
ABCACBBACBCACABCBA

Parallelism

As I mentioned, the Stream we get from Permutations.of() supports parallelism which is nice if you have a large number of permutaions and you want to put all your CPU:s to use. To be able to examine which threads are executing what, we will use a small support method:
private static <T> void printThreadInfo(Stream<T> s) {
    System.out.println(Thread.currentThread().getName() + " handles " + s.collect(toList()));
}
Now, let us examine which threads are being used by running the following line:
Permutations.of("A", "B", "C").forEach(Main::printThreadInfo);
We will see something like this:
main handles [A, B, C]
main handles [A, C, B]
main handles [B, A, C]
main handles [B, C, A]
main handles [C, A, B]
main handles [C, B, A]
This is logical. All our permutations are handled by the same thread (because we did not ask for a parallel stream). Now, let's modify the test line so it looks like this:
Permutations.of("A", "B", "C").parallel().forEach(Main::printThreadInfo);
This will produce something similar to this;
main handles [B, C, A]
main handles [C, B, A]
ForkJoinPool.commonPool-worker-2 handles [C, A, B]
ForkJoinPool.commonPool-worker-2 handles [A, B, C]
ForkJoinPool.commonPool-worker-2 handles [B, A, C]
ForkJoinPool.commonPool-worker-1 handles [A, C, B]
Apparently, on my computer, two combinations continued to execute on the main thread where as there were two other threads that (unevenly) shared the rest of the work.

Laziness

It should be noted that the Stream in Permutations.of() produces the sequence of combinations lazily (or using other words, as they are needed). So, if we set up an enormous amount of combinations, but only need one, the stream will only produce that one and only combination. Let's take an example with 16 input items which corresponds to an extremely large number of permutations, something comparable with the national dept:
System.out.println(
        Permutations.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
        .findFirst()
        .get()                
        .collect(toList())
);
This line will complete instantly and will return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] which, unsurprisingly, is the first combination.

Testing

Permutations are great for testing! How many times have you asked yourself: "Have I tried all the possible combinations". Using Permutations,  you can now be sure that you have tested every input combinations like in this simple outline (obviously, you need to replace System.out.println with something more useful):
Runnable setup = () -> System.out.println("setup connection");
Runnable send = () -> System.out.println("send data");
Runnable close = () -> System.out.println("close connection");

Permutations.of(setup, send, close)
    .flatMap(Function.identity())
    .forEachOrdered(Runnable::run);

The Morning

So, how does the optimal morning look like? Well, if you are alone at home, your day might start in many different ways, perhaps like this:
Permutations.of(
        "Get up",
        "Brush teeths",
        "Eat breakfast",
        "Get dressed",
        "Find wallet",
        "Find computer"
)
        .map(s -> s.collect(toList()))
        .forEach(System.out::println);
So now you can pick your favorite from:
[Get up, Brush teeths, Eat breakfast, Get dressed, Find wallet, Find computer]
[Get up, Brush teeths, Eat breakfast, Get dressed, Find computer, Find wallet]
[Get up, Brush teeths, Eat breakfast, Find wallet, Get dressed, Find computer]
[Get up, Brush teeths, Eat breakfast, Find wallet, Find computer, Get dressed]
[Get up, Brush teeths, Eat breakfast, Find computer, Get dressed, Find wallet]
[Get up, Brush teeths, Eat breakfast, Find computer, Find wallet, Get dressed]

... (713 other combinations) ...

[Find computer, Find wallet, Get dressed, Eat breakfast, Brush teeths, Get up]
With some preparation, even the last combination is really doable...

Conclusions

Permutation is a powerful tool that is worth mastering.

In this post, I have devised a very short and simple, yet reasonably efficient, implementation of a permutation support class for Java 8.

If you are writing unit tests, you should definitely know how to use permutations.

Take care, not to hit the "asymptotic wall" and limit the number of items that you work with when you are generating permutations.


Life is about picking the right choices, is it not?