Hello Ottomata,

I'd like you to do a code review.  Please visit

    https://gerrit.wikimedia.org/r/90167

to review the following change.

Change subject: Add basic test infrastructure
......................................................................

Add basic test infrastructure

Change-Id: I3ba4b78d32fc22a45b8ef43ae6d3794e38803866
---
M kraken-etl/pom.xml
A 
kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/LoggingMockingTestCase.java
A 
kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/MockingTestCase.java
A 
kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/log/CollectionAppender.java
A 
kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/log/LogUtil.java
5 files changed, 446 insertions(+), 0 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/analytics/kraken 
refs/changes/67/90167/1

diff --git a/kraken-etl/pom.xml b/kraken-etl/pom.xml
index 3446124..4fcc6d6 100644
--- a/kraken-etl/pom.xml
+++ b/kraken-etl/pom.xml
@@ -27,6 +27,27 @@
       <version>0.1.0-SNAPSHOT-wmf-1</version>
       <scope>compile</scope>
     </dependency>
+
+    <dependency>
+      <groupId>org.easymock</groupId>
+      <artifactId>easymock</artifactId>
+      <version>3.0</version>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.powermock</groupId>
+      <artifactId>powermock-module-junit4</artifactId>
+      <version>1.5</version>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.powermock</groupId>
+      <artifactId>powermock-api-easymock</artifactId>
+      <version>1.5</version>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
diff --git 
a/kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/LoggingMockingTestCase.java
 
b/kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/LoggingMockingTestCase.java
new file mode 100644
index 0000000..fc199a0
--- /dev/null
+++ 
b/kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/LoggingMockingTestCase.java
@@ -0,0 +1,121 @@
+// Copyright (C) 2013 Wikimedia Foundation
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
USA.
+
+package org.wikimedia.analytics.kraken.etl.testutil;
+
+import com.google.common.collect.Lists;
+
+import org.wikimedia.analytics.kraken.etl.testutil.log.LogUtil;
+import org.apache.log4j.spi.LoggingEvent;
+import org.junit.After;
+
+import java.util.Iterator;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+
+public abstract class LoggingMockingTestCase extends MockingTestCase {
+  private String loggerName;
+  private LogUtil.LoggerSettings loggerSettings;
+  private java.util.Collection<LoggingEvent> loggedEvents;
+
+  protected final void assertLogMessageContains(String needle, Level level) {
+    LoggingEvent hit = null;
+    Iterator<LoggingEvent> iter = loggedEvents.iterator();
+    while (hit == null && iter.hasNext()) {
+      LoggingEvent event = iter.next();
+      if (event.getRenderedMessage().contains(needle)) {
+        if (level == null || level.equals(event.getLevel())) {
+          hit = event;
+        }
+      }
+    }
+    assertNotNull("Could not find log message containing '" + needle + "'",
+        hit);
+    assertTrue("Could not remove log message containing '" + needle + "'",
+        loggedEvents.remove(hit));
+  }
+
+  protected final void assertLogMessageContains(String needle) {
+    assertLogMessageContains(needle, null);
+  }
+
+  protected final void assertLogThrowableMessageContains(String needle) {
+    LoggingEvent hit = null;
+    Iterator<LoggingEvent> iter = loggedEvents.iterator();
+    while (hit == null && iter.hasNext()) {
+      LoggingEvent event = iter.next();
+
+      if (event.getThrowableInformation().getThrowable().toString()
+          .contains(needle)) {
+        hit = event;
+      }
+    }
+    assertNotNull("Could not find log message with a Throwable containing '"
+        + needle + "'", hit);
+    assertTrue("Could not remove log message with a Throwable containing '"
+        + needle + "'", loggedEvents.remove(hit));
+  }
+
+  // As the PowerMock runner does not pass through runTest, we inject log
+  // verification through @After
+  @After
+  public final void assertNoUnassertedLogEvents() {
+    if (loggedEvents.size() > 0) {
+      LoggingEvent event = loggedEvents.iterator().next();
+      String msg = "Found untreated logged events. First one is:\n";
+      msg += event.getRenderedMessage();
+      if (event.getThrowableInformation() != null) {
+        msg += "\n" + event.getThrowableInformation().getThrowable();
+      }
+      fail(msg);
+    }
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    loggedEvents = Lists.newArrayList();
+
+    // The logger we're interested is class name without the trailing "Test".
+    // While this is not the most general approach it is sufficient for now,
+    // and we can improve later to allow tests to specify which loggers are
+    // to check.
+    loggerName = this.getClass().getCanonicalName();
+    loggerName = loggerName.substring(0, loggerName.length()-4);
+    loggerSettings = LogUtil.logToCollection(loggerName, loggedEvents);
+  }
+
+  @Override
+  protected void runTest() throws Throwable {
+    super.runTest();
+    // Plain JUnit runner does not pick up @After, so we add it here
+    // explicitly. Note, that we cannot put this into tearDown, as failure
+    // to verify mocks would bail out and might leave open resources from
+    // subclasses open.
+    assertNoUnassertedLogEvents();
+  }
+
+  @Override
+  public void tearDown() throws Exception {
+    if (loggerName != null && loggerSettings != null) {
+      Logger logger = LogManager.getLogger(loggerName);
+      loggerSettings.pushOntoLogger(logger);
+    }
+    super.tearDown();
+  }
+}
diff --git 
a/kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/MockingTestCase.java
 
b/kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/MockingTestCase.java
new file mode 100644
index 0000000..f2cb3c7
--- /dev/null
+++ 
b/kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/MockingTestCase.java
@@ -0,0 +1,155 @@
+// Copyright (C) 2013 Wikimedia Foundation
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
USA.
+
+package org.wikimedia.analytics.kraken.etl.testutil;
+
+import junit.framework.TestCase;
+
+import org.easymock.EasyMock;
+import org.easymock.IMocksControl;
+import org.junit.After;
+import org.junit.runner.RunWith;
+import org.powermock.api.easymock.PowerMock;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Test case with some support for automatically verifying mocks.
+ */
+public abstract class MockingTestCase extends TestCase {
+  private Collection<Object> mocks;
+  private Collection<IMocksControl> mockControls;
+  private boolean mocksReplayed;
+  private boolean usePowerMock;
+
+  /**
+   * Create and register a mock control.
+   *
+   * @return The mock control instance.
+   */
+  protected final IMocksControl createMockControl() {
+    IMocksControl mockControl = EasyMock.createControl();
+    assertTrue("Adding mock control failed", mockControls.add(mockControl));
+    return mockControl;
+  }
+
+  /**
+   * Create and register a mock.
+   *
+   * Creates a mock and registers it in the list of created mocks, so it gets
+   * treated automatically upon {@code replay} and {@code verify};
+   * @param toMock The class to create a mock for.
+   * @return The mock instance.
+   */
+  protected final <T> T createMock(Class<T> toMock) {
+    return createMock(toMock, null);
+  }
+
+  /**
+   * Create a mock for a mock control and register a mock.
+   *
+   * Creates a mock and registers it in the list of created mocks, so it gets
+   * treated automatically upon {@code replay} and {@code verify};
+   * @param toMock The class to create a mock for.
+   * @param control The mock control to create the mock on. If null, do not use
+   *    a specific control.
+   * @return The mock instance.
+   */
+  protected final <T> T createMock(Class<T> toMock, IMocksControl control) {
+    assertFalse("Mocks have already been set to replay", mocksReplayed);
+    final T mock;
+    if (control == null) {
+      if (usePowerMock) {
+        mock = PowerMock.createMock(toMock);
+      } else {
+        mock = EasyMock.createMock(toMock);
+      }
+      assertTrue("Adding " + toMock.getName() + " mock failed",
+          mocks.add(mock));
+    } else {
+      mock = control.createMock(toMock);
+    }
+    return mock;
+  }
+
+  /**
+   * Set all registered mocks to replay
+   */
+  protected final void replayMocks() {
+    assertFalse("Mocks have already been set to replay", mocksReplayed);
+    if (usePowerMock) {
+      PowerMock.replayAll();
+    } else {
+      EasyMock.replay(mocks.toArray());
+    }
+    for (IMocksControl mockControl : mockControls) {
+      mockControl.replay();
+    }
+    mocksReplayed = true;
+  }
+
+  /**
+   * Verify all registered mocks
+   *
+   * This method is called automatically at the end of a test. Nevertheless,
+   * it is safe to also call it beforehand, if this better meets the
+   * verification part of a test.
+   */
+  // As the PowerMock runner does not pass through runTest, we inject mock
+  // verification through @After
+  @After
+  public final void verifyMocks() {
+    if (!mocks.isEmpty() || !mockControls.isEmpty()) {
+      assertTrue("Created mocks have not been set to replay. Call replayMocks "
+          + "within the test", mocksReplayed);
+      if (usePowerMock) {
+        PowerMock.verifyAll();
+      } else {
+        EasyMock.verify(mocks.toArray());
+      }
+      for (IMocksControl mockControl : mockControls) {
+        mockControl.verify();
+      }
+    }
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+
+    usePowerMock = false;
+    RunWith runWith = this.getClass().getAnnotation(RunWith.class);
+    if (runWith != null) {
+      usePowerMock = PowerMockRunner.class.isAssignableFrom(runWith.value());
+    }
+
+    mocks = new ArrayList<Object>();
+    mockControls = new ArrayList<IMocksControl>();
+    mocksReplayed = false;
+  }
+
+  @Override
+  protected void runTest() throws Throwable {
+    super.runTest();
+    // Plain JUnit runner does not pick up @After, so we add it here
+    // explicitly. Note, that we cannot put this into tearDown, as failure
+    // to verify mocks would bail out and might leave open resources from
+    // subclasses open.
+    verifyMocks();
+  }
+}
diff --git 
a/kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/log/CollectionAppender.java
 
b/kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/log/CollectionAppender.java
new file mode 100644
index 0000000..948aa28
--- /dev/null
+++ 
b/kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/log/CollectionAppender.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2013 Wikimedia Foundation
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
USA.
+
+package org.wikimedia.analytics.kraken.etl.testutil.log;
+
+import com.google.common.collect.Lists;
+
+import org.apache.log4j.AppenderSkeleton;
+import org.apache.log4j.spi.LoggingEvent;
+
+import java.util.Collection;
+import java.util.LinkedList;
+
+/**
+ * Log4j appender that logs into a list
+ */
+public class CollectionAppender extends AppenderSkeleton {
+  private Collection<LoggingEvent> events;
+
+  public CollectionAppender() {
+    events = new LinkedList<LoggingEvent>();
+  }
+
+  public CollectionAppender(Collection<LoggingEvent> events) {
+    this.events = events;
+  }
+
+  @Override
+  public boolean requiresLayout() {
+    return false;
+  }
+
+  @Override
+  protected void append(LoggingEvent event) {
+    if (! events.add(event)) {
+      throw new RuntimeException("Could not append event " + event);
+    }
+  }
+
+  @Override
+  public void close() {
+  }
+
+  public Collection<LoggingEvent> getLoggedEvents() {
+    return Lists.newLinkedList(events);
+  }
+}
diff --git 
a/kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/log/LogUtil.java
 
b/kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/log/LogUtil.java
new file mode 100644
index 0000000..12bb3fd
--- /dev/null
+++ 
b/kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/log/LogUtil.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2013 Wikimedia Foundation
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
USA.
+
+package org.wikimedia.analytics.kraken.etl.testutil.log;
+
+import org.apache.log4j.Appender;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import org.apache.log4j.spi.LoggingEvent;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.List;
+
+public class LogUtil {
+  /**
+   * Change logger's setting so it only logs to a collection.
+   *
+   * @param logName Name of the logger to modify.
+   * @param collection The collection to log into.
+   * @return The logger's original settings.
+   */
+  public static LoggerSettings logToCollection(String logName,
+      Collection<LoggingEvent> collection) {
+    Logger logger = LogManager.getLogger(logName);
+    LoggerSettings loggerSettings = new LoggerSettings(logger);
+    logger.removeAllAppenders();
+    logger.setAdditivity(false);
+    CollectionAppender listAppender = new CollectionAppender(collection);
+    logger.addAppender(listAppender);
+    return loggerSettings;
+  }
+
+  /**
+   * Capsule for a logger's settings that get mangled by rerouting logging to 
a collection
+   */
+  public static class LoggerSettings {
+    private final boolean additive;
+    private final List<Appender> appenders;
+
+    /**
+     * Read off logger settings from an instance.
+     *
+     * @param logger The logger to read the settings off from.
+     */
+    private LoggerSettings(Logger logger) {
+        this.additive = logger.getAdditivity();
+
+        Enumeration<?> appenders = logger.getAllAppenders();
+        this.appenders = new ArrayList<Appender>();
+        while (appenders.hasMoreElements()) {
+            Object appender = appenders.nextElement();
+            if (appender instanceof Appender) {
+                this.appenders.add((Appender)appender);
+            } else {
+                throw new RuntimeException("getAllAppenders of " + logger
+                        + " contained an object that is not an Appender");
+            }
+        }
+    }
+
+    /**
+     * Pushes this settings back onto a logger.
+     *
+     * @param logger the logger on which to push the settings.
+     */
+    public void pushOntoLogger(Logger logger) {
+      logger.setAdditivity(additive);
+             logger.removeAllAppenders();
+      for (Appender appender : appenders) {
+        logger.addAppender(appender);
+      }
+    }
+  }
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/90167
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I3ba4b78d32fc22a45b8ef43ae6d3794e38803866
Gerrit-PatchSet: 1
Gerrit-Project: analytics/kraken
Gerrit-Branch: master
Gerrit-Owner: QChris <[email protected]>
Gerrit-Reviewer: Ottomata <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to