11
\$\begingroup\$

This question continues on my previous implementations of the Hand class of a Trading Card Game, earlier questions can be found here:

I have tried to implement as much of the old reviews as I thought was neccessary, except the point about the unit test method names, I haven't had time to do that yet.

The goal of this implementation is to allow concrete instances of a HandView to listen for add, play and swap events on the Hand class. I expect concrete implementations to work together with for example the console or a GUI to relay the information back to the user.

I will first list some dependency classes without unit tests, and then my implementations with unit tests.

Dependencies:

public final class Arguments {
    private Arguments() {
        throw new UnsupportedOperationException();
    }

    public static int requirePositive(final int value) throws IllegalArgumentException {
        return requirePositive(value, "value");
    }

    public static int requirePositive(final int value, final String name) throws IllegalArgumentException {
        Objects.requireNonNull(name);
        if (value <= 0) {
            throw new IllegalArgumentException("the " + name + " must be positive: " + value);
        }
        return value;
    }

    public static int requireNegative(final int value) throws IllegalArgumentException {
        return requireNegative(value, "value");
    }

    public static int requireNegative(final int value, final String name) throws IllegalArgumentException {
        Objects.requireNonNull(name);
        if (value >= 0) {
            throw new IllegalArgumentException("the " + name + " must be negative: " + value);
        }
        return value;
    }

    public static int requirePositiveOrZero(final int value) throws IllegalArgumentException {
        return requirePositiveOrZero(value, "value");
    }

    public static int requirePositiveOrZero(final int value, final String name) throws IllegalArgumentException {
        Objects.requireNonNull(name);
        if (value < 0) {
            throw new IllegalArgumentException("the " + name + " must be positive or zero: " + value);
        }
        return value;
    }

    public static int requireNegativeOrZero(final int value) throws IllegalArgumentException {
        return requireNegativeOrZero(value, "value");
    }

    public static int requireNegativeOrZero(final int value, final String name) throws IllegalArgumentException {
        Objects.requireNonNull(name);
        if (value > 0) {
            throw new IllegalArgumentException("the " + name + " must be negative or zero: " + value);
        }
        return value;
    }

    public static int requireInRange(final int value, final int lowInclusive, final int highExclusive) throws IllegalArgumentException {
        return requireInRange(value, lowInclusive, highExclusive, "value");
    }

    public static int requireInRange(final int value, final int lowInclusive, final int highExclusive, final String name) throws IllegalArgumentException {
        Objects.requireNonNull(name);
        if (lowInclusive >= highExclusive) {
            throw new IllegalArgumentException("the lower inclusive bound is greater or equal to the higher exclusive bound: " + lowInclusive + " >= " + highExclusive);
        }
        if (value < lowInclusive || value >= highExclusive) {
            throw new IllegalArgumentException("the " + name + " must be in range: " + value + ", expected: [" + lowInclusive + ", " + highExclusive + ")");
        }
        return value;
    }

    public static int requireInRangeClosed(final int value, final int lowInclusive, final int highInclusive) throws IllegalArgumentException {
        return requireInRangeClosed(value, lowInclusive, highInclusive, "value");
    }

    public static int requireInRangeClosed(final int value, final int lowInclusive, final int highInclusive, final String name) throws IllegalArgumentException {
        Objects.requireNonNull(name);
        if (lowInclusive > highInclusive) {
            throw new IllegalArgumentException("the lower inclusive bound is greater or equal to the higher inclusive bound: " + lowInclusive + " >= " + highInclusive);
        }
        if (value < lowInclusive || value > highInclusive) {
            throw new IllegalArgumentException("the " + name + " must be in range: " + value + ", expected: [" + lowInclusive + ", " + highInclusive + ")]");
        }
        return value;
    }

    public static int requireIndexInRange(final int index, final int lowInclusive, final int highExclusive) throws IndexOutOfBoundsException {
        if (index < lowInclusive || index >= highExclusive) {
            throw new IndexOutOfBoundsException("the index must be in range: " + index + ", expected: [" + lowInclusive + ", " + highExclusive + ")");
        }
        return index;
    }

    public static int requireIndexInRangeClosed(final int index, final int lowInclusive, final int highInclusive) throws IndexOutOfBoundsException {
        if (index < lowInclusive || index > highInclusive) {
            throw new IndexOutOfBoundsException("the index must be in range: " + index + ", expected: [" + lowInclusive + ", " + highInclusive + "]");
        }
        return index;
    }

    public static String requireMinimalLength(final String value, final int minimum) throws IllegalArgumentException {
        return requireMinimalLength(value, minimum, "value");
    }

    public static String requireMinimalLength(final String value, final int minimum, final String name) throws IllegalArgumentException {
        Objects.requireNonNull(value);
        Arguments.requirePositive(minimum, "minimum");
        Objects.requireNonNull(name);
        if (value.length() < minimum) {
            throw new IllegalArgumentException("the length of the " + name + " must be at least the minimum: " + value.length() + ", expected: " + minimum);
        }
        return value;
    }
}

public final class States {
    private States() {
        throw new UnsupportedOperationException();
    }

    public static boolean requireTrue(final boolean condition) throws IllegalStateException {
        return requireTrue(condition, "condition must be true");
    }

    public static boolean requireTrue(final boolean condition, final String message) throws IllegalStateException {
        Objects.requireNonNull(message);
        if (!condition) {
            throw new IllegalStateException(message);
        }
        return condition;
    }

    public static boolean requireFalse(final boolean condition) throws IllegalStateException {
        return requireFalse(condition, "condition must be false");
    }

    public static boolean requireFalse(final boolean condition, final String message) throws IllegalStateException {
        Objects.requireNonNull(message);
        if (condition) {
            throw new IllegalStateException(message);
        }
        return condition;
    }

    public static <E, C extends Collection<E>> C requireEmpty(final C collection) throws IllegalStateException {
        return requireEmpty(collection, "collection");
    }

    public static <E, C extends Collection<E>> C requireEmpty(final C collection, final String name) throws IllegalStateException {
        Objects.requireNonNull(name);
        if (!collection.isEmpty()) {
            throw new IllegalStateException(name + " must be empty");
        }
        return collection;
    }

    public static <E, C extends Collection<E>> C requireNonEmpty(final C collection) throws NoSuchElementException {
        return requireNonEmpty(collection, "collection");
    }

    public static <E, C extends Collection<E>> C requireNonEmpty(final C collection, final String name) throws NoSuchElementException {
        Objects.requireNonNull(name);
        if (collection.isEmpty()) {
            throw new NoSuchElementException(name + " must be non-empty");
        }
        return collection;
    }

    public static <T, E extends RuntimeException> T requireNonNull(final T object, final Supplier<E> exceptionSupplier) throws E {
        Objects.requireNonNull(exceptionSupplier);
        if (object == null) {
            throw exceptionSupplier.get();
        }
        return object;
    }

    public static <T, E extends RuntimeException> T requireNonNull(final T object, final Function<String, E> exceptionFunction, final String message) throws E {
        Objects.requireNonNull(exceptionFunction);
        Objects.requireNonNull(message);
        if (object == null) {
            throw exceptionFunction.apply(message);
        }
        return object;
    }
}

Interfaces for the View implementation:

/**
 * A marker interface to denote that an object implements a view on some other object.
 *
 * @author Frank van Heeswijk
 * @param <T>   The type of object that is viewed
 */
public interface View<T extends Viewable<T, ? extends View<T>>> {

}

/**
 * An interface for objects that are viewable via a view.
 * 
 * @author Frank van Heeswijk
 * @param <T>   The type of viewable object
 * @param <V>   The concrete view on the viewable object
 */
public interface Viewable<T extends Viewable<T, V>, V extends View<T>> {
    public void addViewCallback(final V view);

    public void removeViewCallback(final V view);
}

Concrete implementations:

public interface HandView extends View<Hand> {
    public void onCardAdded(final Card card);

    public void onCardPlayed(final int cardIndex);

    public void onCardsSwapped(final int cardIndexOne, final int cardIndexTwo);
}

public class Hand extends AbstractCollection<Card> implements Collection<Card>, Viewable<Hand, HandView> {
    private final Collection<HandView> views = new HashSet<>();

    private final int capacity;
    private final List<Card> list;

    public Hand(final int capacity) {
        this.capacity = Arguments.requirePositive(capacity, "capacity");
        this.list = new ArrayList<>(capacity);
    }

    @Override
    public void addViewCallback(final HandView view) {
        views.add(Objects.requireNonNull(view));
    }

    @Override
    public void removeViewCallback(final HandView view) {
        if (!views.remove(Objects.requireNonNull(view))) {
            throw new IllegalStateException("the requested view to remove must be present in the views: " + view);
        }
    }

    public boolean isFull() {
        return (list.size() == capacity);
    }

    @Override
    public boolean add(final Card card) {
        Objects.requireNonNull(card);
        States.requireFalse(isFull(), "hand is full");
        list.add(card);
        views.forEach(view -> view.onCardAdded(card));
        return true;
    }

    public Card get(final int index) {
        checkIndex(index);
        return list.get(index);
    }

    public Card play(final int index) {
        checkIndex(index);
        Card result = list.remove(index);
        views.forEach(view -> view.onCardPlayed(index));
        return result;
    }

    public void swap(final int indexOne, final int indexTwo) {
        checkIndex(indexOne);
        checkIndex(indexTwo);
        Collections.swap(list, indexOne, indexTwo);
        views.forEach(view -> view.onCardsSwapped(indexOne, indexTwo));
    }

    @Override
    public String toString() {
        return Hand.class.getSimpleName() + "(" + capacity + ", " + list + ")";
    }

    @Override
    public Iterator<Card> iterator() {
        return list.iterator();
    }

    @Override
    public Spliterator<Card> spliterator() {
        return list.spliterator();
    }

    @Override
    public int size() {
        return list.size();
    }

    @Override
    public void forEach(final Consumer<? super Card> action) {
        Objects.requireNonNull(action);
        list.forEach(action);
    }

    private void checkIndex(final int index) {
        Arguments.requireIndexInRange(index, 0, size());
    }
}

Unit test for Hand:

public class HandTest {
    static {
        assertTrue(true);
    }

    @Test
    public void testConstructor() {
        new Hand(1);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testConstructorIAE() {
        new Hand(0);
    }

    @Test
    public void testAddViewCallback() {
        Hand hand = new Hand(5);
        hand.addViewCallback(createHandView());
    }

    @Test(expected = NullPointerException.class)
    public void testAddViewCallbackViewNull() {
        Hand hand = new Hand(5);
        hand.addViewCallback(null);
    }

    @Test
    public void testRemoveViewCallback() {
        Hand hand = new Hand(5);
        HandView handView = createHandView();
        hand.addViewCallback(handView);
        hand.removeViewCallback(handView);
    }

    @Test(expected = NullPointerException.class)
    public void testRemoveViewCallbackViewNull() {
        Hand hand = new Hand(5);
        hand.removeViewCallback(null);
    }

    @Test(expected = IllegalStateException.class)
    public void testRemoveViewCallbackViewNotPresent() {
        Hand hand = new Hand(5);
        hand.removeViewCallback(createHandView());
    }

    @Test
    public void testIsFull() {
        Hand hand = new Hand(2);
        hand.add(createCard());
        assertFalse("hand should not be full", hand.isFull());
        hand.add(createCard());
        assertTrue("hand should be full", hand.isFull());
        hand.play(1);
        assertFalse("hand should not be full anymore", hand.isFull());
    }

    @Test
    public void testAdd() {
        Hand hand = new Hand(1);
        assertTrue(hand.add(createCard()));
    }

    @Test(expected = NullPointerException.class)
    public void testAddNPE() {
        Hand hand = new Hand(1);
        hand.add(null);
    }

    @Test(expected = IllegalStateException.class)
    public void testAddISE() {
        Hand hand = new Hand(1);
        hand.add(createCard());
        hand.add(createCard());
    }

    @Test
    public void testGet() {
        Hand hand = new Hand(1);
        Card card = createCard();
        hand.add(card);
        assertEquals("card should be equal", card, hand.get(0));
    }

    @Test(expected = IndexOutOfBoundsException.class)
    public void testGetIOOBE() {
        Hand hand = new Hand(1);
        hand.get(0);
    }

    @Test
    public void testPlay() {
        Hand hand = new Hand(1);
        Card card = createCard();
        hand.add(card);
        assertEquals("card should be equal", card, hand.play(0));
    }

    @Test(expected = IndexOutOfBoundsException.class)
    public void testPlayIOOBE1() {
        Hand hand = new Hand(1);
        hand.play(-1);
    }

    @Test(expected = IndexOutOfBoundsException.class)
    public void testPlayIOOBE2() {
        Hand hand = new Hand(1);
        hand.play(0);
    }

    @Test(expected = IndexOutOfBoundsException.class)
    public void testPlayIOOB3() {
        Hand hand = new Hand(1);
        hand.add(createCard());
        hand.play(1);
    }

    @Test
    public void testSwap() {
        Hand hand = new Hand(2);
        Card card = createCard();
        Card card2 = createCard2();
        hand.add(card);
        hand.add(card2);
        assertNotSame("card should be unequal to card2", card, card2);
        hand.swap(0, 1);
        assertEquals("card should be equal", card, hand.play(1));
        assertEquals("card2 should be equal", card2, hand.play(0));
    }

    @Test(expected = IndexOutOfBoundsException.class)
    public void testSwapIOOBE1() {
        Hand hand = new Hand(1);
        hand.add(createCard());
        hand.swap(-1, 0);
    }

    @Test(expected = IndexOutOfBoundsException.class)
    public void testSwapIOOBE2() {
        Hand hand = new Hand(1);
        hand.add(createCard());
        hand.swap(1, 0);
    }

    @Test(expected = IndexOutOfBoundsException.class)
    public void testSwapIOOBE3() {
        Hand hand = new Hand(1);
        hand.add(createCard());
        hand.swap(0, -1);
    }

    @Test(expected = IndexOutOfBoundsException.class)
    public void testSwapIOOBE4() {
        Hand hand = new Hand(1);
        hand.add(createCard());
        hand.swap(0, 1);
    }

    @Test
    public void testToString1() {
        Hand hand = new Hand(1);
        assertEquals(Hand.class.getSimpleName() + "(1, [])", hand.toString());
    }

    @Test
    public void testToString2() {
        Hand hand = new Hand(2);
        Card card = createCard();
        Card card2 = createCard2();
        assertNotSame("card should be unequal to card2", card, card2);
        hand.add(card);
        hand.add(card2);
        assertEquals(Hand.class.getSimpleName() + "(2, [" + card + ", " + card2 + "])", hand.toString());
    }

    @Test
    public void testIterator() {
        Hand hand = new Hand(2);
        Card card = createCard();
        Card card2 = createCard2();
        hand.add(card);
        hand.add(card2);
        assertNotSame("card should be unequal to card2", card, card2);
        Iterator<Card> iterator = hand.iterator();
        assertTrue(iterator.hasNext());
        assertEquals("first element should equal card", card, iterator.next());
        assertTrue(iterator.hasNext());
        assertEquals("second element should equal card2", card2, iterator.next());
        assertFalse(iterator.hasNext());
    }

    @Test
    public void testSpliterator() {
        assertNotNull(createFilledHand().spliterator());
    }

    @Test
    public void testSize() {
        Hand hand = new Hand(2);
        assertEquals("empty hand", 0, hand.size());
        hand.add(createCard());
        assertEquals("one card", 1, hand.size());
        hand.add(createCard2());
        assertEquals("two cards", 2, hand.size());
    }

    @Test
    public void testForEach() {
        createFilledHand().forEach(Assert::assertNotNull);
    }

    private Card createCard() {
        return new MonsterCard("Test", 10, 100, MonsterModus.OFFENSIVE);
    }

    private Card createCard2() {
        return new MonsterCard("Test2", 15, 150, MonsterModus.HEALING);
    }

    private Hand createFilledHand() {
        Hand hand = new Hand(2);
        Card card = createCard();
        Card card2 = createCard2();
        assertNotSame("card should be unequal to card2", card, card2);
        hand.add(card);
        hand.add(card2);
        return hand;
    }

    private HandView createHandView() {
        return new HandView() {
            @Override
            public void onCardAdded(final Card card) { }

            @Override
            public void onCardPlayed(final int cardIndex) { }

            @Override
            public void onCardsSwapped(final int cardIndexOne, final int cardIndexTwo) { }
        };
    }
}

Integration test for Hand, integrating with HandView:

public class HandIT {
    static {
        assertTrue(true);
    }

    @Test
    public void testAdd() {
        Hand hand = new Hand(5);
        final AtomicInteger counter = new AtomicInteger(0);
        final AtomicReference<Card> cardReferenceOne = new AtomicReference<>();
        final AtomicReference<Card> cardReferenceTwo = new AtomicReference<>();

        hand.addViewCallback(new HandView() {
            @Override
            public void onCardAdded(final Card card) {
                counter.incrementAndGet();
                cardReferenceOne.set(card);
            }

            @Override
            public void onCardPlayed(final int cardIndex) { }

            @Override
            public void onCardsSwapped(final int cardIndexOne, final int cardIndexTwo) { }
        });
        hand.addViewCallback(new HandView() {
            @Override
            public void onCardAdded(final Card card) {
                counter.incrementAndGet();
                cardReferenceTwo.set(card);
            }

            @Override
            public void onCardPlayed(final int cardIndex) { }

            @Override
            public void onCardsSwapped(final int cardIndexOne, final int cardIndexTwo) { }
        });

        Card card = new MonsterCard("Test", 5, 5, MonsterModus.HEALING);
        hand.add(card);

        assertEquals(2, counter.get());
        assertEquals(card, cardReferenceOne.get());
        assertEquals(card, cardReferenceTwo.get());
    }

    @Test
    public void testPlay() {
        Hand hand = new Hand(5);
        hand.add(new MonsterCard("Test", 5, 5, MonsterModus.HEALING));
        final AtomicInteger counter = new AtomicInteger(0);
        final AtomicInteger indexReferenceOne = new AtomicInteger(-1);
        final AtomicInteger indexReferenceTwo = new AtomicInteger(-1);

        hand.addViewCallback(new HandView() {
            @Override
            public void onCardAdded(final Card card) { }

            @Override
            public void onCardPlayed(final int cardIndex) {
                counter.incrementAndGet();
                indexReferenceOne.set(cardIndex);
            }

            @Override
            public void onCardsSwapped(final int cardIndexOne, final int cardIndexTwo) { }
        });
        hand.addViewCallback(new HandView() {
            @Override
            public void onCardAdded(final Card card) { }

            @Override
            public void onCardPlayed(final int cardIndex) {
                counter.incrementAndGet();
                indexReferenceTwo.set(cardIndex);
            }

            @Override
            public void onCardsSwapped(final int cardIndexOne, final int cardIndexTwo) { }
        });

        hand.play(0);

        assertEquals(2, counter.get());
        assertEquals(0, indexReferenceOne.get());
        assertEquals(0, indexReferenceTwo.get());
    }

    @Test
    public void testSwap() {
        Hand hand = new Hand(5);
        hand.add(new MonsterCard("Test", 5, 5, MonsterModus.HEALING));
        hand.add(new MonsterCard("Random", 3, 7, MonsterModus.OFFENSIVE));
        final AtomicInteger counter = new AtomicInteger(0);
        final AtomicInteger indexOneReferenceOne = new AtomicInteger(-1);
        final AtomicInteger indexTwoReferenceOne = new AtomicInteger(-1);
        final AtomicInteger indexOneReferenceTwo = new AtomicInteger(-1);
        final AtomicInteger indexTwoReferenceTwo = new AtomicInteger(-1);

        hand.addViewCallback(new HandView() {
            @Override
            public void onCardAdded(final Card card) { }

            @Override
            public void onCardPlayed(final int cardIndex) { }

            @Override
            public void onCardsSwapped(final int cardIndexOne, final int cardIndexTwo) {
                counter.incrementAndGet();
                indexOneReferenceOne.set(cardIndexOne);
                indexTwoReferenceOne.set(cardIndexTwo);
            }
        });
        hand.addViewCallback(new HandView() {
            @Override
            public void onCardAdded(final Card card) { }

            @Override
            public void onCardPlayed(final int cardIndex) { }

            @Override
            public void onCardsSwapped(final int cardIndexOne, final int cardIndexTwo) {
                counter.incrementAndGet();
                indexOneReferenceTwo.set(cardIndexOne);
                indexTwoReferenceTwo.set(cardIndexTwo);
            }
        });

        hand.swap(0, 1);

        assertEquals(2, counter.get());
        assertEquals(0, indexOneReferenceOne.get());
        assertEquals(1, indexTwoReferenceOne.get());
        assertEquals(0, indexOneReferenceTwo.get());
        assertEquals(1, indexTwoReferenceTwo.get());
    }
}

The unit tests hit 100% coverage as intended, and they also all pass.

Reviews about every aspect are welcome. Extra attention would be nice for the integration tests, as they are my first integration tests ever.

\$\endgroup\$
5
  • 1
    \$\begingroup\$ Where's the assertion in testRemoveViewCallback? \$\endgroup\$ Apr 30 '14 at 15:27
  • \$\begingroup\$ @abuzittingillifirca Can you please elaborate on what you exactly mean? \$\endgroup\$
    – skiwi
    Apr 30 '14 at 15:34
  • 3
    \$\begingroup\$ Every test should have an assertion, otherwise it is useless. A remove listener test scenario should be something like "When I remove a listener and raise an event (then) the listener should not be notified anymore." The part after then is the assertion. You should make each test fail first, to ensure it does something useful. \$\endgroup\$ Apr 30 '14 at 15:47
  • \$\begingroup\$ @abuzittingillifirca It's a fair point to have in a review, I do think it would be more suited for the integration tests though, as in the unit test (testRemoveViewCallback), all dependencies should be mocked and should not influence the unit test. \$\endgroup\$
    – skiwi
    Apr 30 '14 at 15:48
  • \$\begingroup\$ Look at here EventBus.unregister for an example. \$\endgroup\$ Apr 30 '14 at 15:56
3
\$\begingroup\$
public interface Viewable<T extends Viewable<T, V>, V extends View<T>> {
    public void addViewCallback(final V view);

    public void removeViewCallback(final V view);
}

Drop the public keywords from the methods. Methods in interfaces are automatically public. See this SO thread for reference.

@Test(expected = IllegalArgumentException.class)

As mentioned in another review, there are good reasons to choose ExpectedException rules over @Test(expected = …).

public void testConstructorIAE() {

Maybe a matter of taste, but I don't like these acronyms for exceptions. If you read it for the first time, it's definitely something you have to think about for half a second.

@Test(expected = NullPointerException.class)
public void testAddViewCallbackViewNull() {
    Hand hand = new Hand(5);
    hand.addViewCallback(null);
}

I still think your job should be preventing internal NPEs to shoot back to the caller. NullPointerException is uselessly generic. Your method knows what went wrong, so give the caller better feedback.

I also agree with what was said in the comments: A test that doesn't assert is useless. I know it seems like "but it's okay here" – I used to think like that and wrote tests like that. Granted, I work with a larger code base, but I ended up finding my own tests that had slightly changed over time to be useless because of this.

I also agree with the other thing from the comments – testXyz is a good name for simple unit tests. Integration tests should have clear, descriptive names. In our project, we use a givenXyzWhenXyzThenXyz pattern. Of course you don't have to stick to this, it's just one of many possibilites.

The nice thing about this pattern is that it forces you to state the abstraction of what is to be asserted in the test name. And really, that is what integration tests do: they don't test the tiny little innards of the system – they test the big picture. On top of that, such proper test names – again, speaking heavily from experience here – make it drastically easier to quickly understand what a test does.

Really, the problem with integration tests is that they are more complex than unit tests and therefore need time to be understood. And just like you want clear method names in the production code to abstract the implementation details away, you should go for clear names in tests that do the same.

Just FYI, our test names can be really long. Really long. Something along the lines of

@Test
public void givenTwoBusinessRequestsViaSoapWithSameDataWhenExecuteRequestProcessingThenSecondRequestIsignored() {
    // …
}

is common and by far not the longest. So don't feel bad giving your tests long names – descriptive is more important.

The last thing in the comment mentions that you should write tests first, make sure they fail and then work on making them pass. While I agree that this is an effective approach, this so-called Test-Driven Development (TDD) is not "mandatory". But I definitely do recommend it!

Other than these things, it looks solid to me. I didn't look at the dependency/helper classes since I assume they didn't change.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ I agree, except one thing: a @Test without assertion is not always useless. It verifies that the operations succeed and don't throw exceptions. If nothing else, it confirms the setup is sane (no exceptions in @Before*). \$\endgroup\$
    – janos
    May 2 '14 at 14:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.