Separate concerns: provide usage data vs publish usage data

Let the data providers just provide data and have the data publisher
deal with publishing the data. This greatly improves the testability.

Change-Id: If2da4b53979f97ead78821ef259327afbba208a5
Signed-off-by: Adrian Görler <adrian.goerler@sap.com>
diff --git a/BUCK b/BUCK
index 2cb7dbe..d16b966 100644
--- a/BUCK
+++ b/BUCK
@@ -27,6 +27,9 @@
   deps = GERRIT_PLUGIN_API + [
     ':quota__plugin',
     '//lib:junit',
+    '//lib/easymock:easymock',
+    '//lib/log:log4j',
+    '//lib/log:impl_log4j',
   ],
   source_under_test = [':quota__plugin'],
 )
diff --git a/lib/BUCK b/lib/BUCK
index dc55495..6959cde 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -4,6 +4,7 @@
   name = 'junit',
   id = 'junit:junit:4.11',
   sha1 = '4e031bb61df09069aeb2bffb4019e7a5034a4ee0',
+  attach_source = False,
   license = 'DO_NOT_DISTRIBUTE',
   deps = [':hamcrest-core'],
 )
@@ -12,6 +13,7 @@
   name = 'hamcrest-core',
   id = 'org.hamcrest:hamcrest-core:1.3',
   sha1 = '42a25dc3219429f0e5d060061f71acb49bf010a0',
+  attach_source = False,
   license = 'DO_NOT_DISTRIBUTE',
   visibility = ['//lib:junit'],
 )
diff --git a/lib/commons/BUCK b/lib/commons/BUCK
index 43ba8aa..be9dfe5 100644
--- a/lib/commons/BUCK
+++ b/lib/commons/BUCK
@@ -5,6 +5,7 @@
   id = 'commons-lang:commons-lang:2.5',
   sha1 = 'b0236b252e86419eef20c31a44579d2aee2f0a69',
   exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'],
+  attach_source = False,
   license = 'Apache2.0',
 )
 
diff --git a/lib/easymock/BUCK b/lib/easymock/BUCK
new file mode 100644
index 0000000..3cf45ae
--- /dev/null
+++ b/lib/easymock/BUCK
@@ -0,0 +1,31 @@
+include_defs('//bucklets/maven_jar.bucklet')
+
+maven_jar(
+  name = 'easymock',
+  id = 'org.easymock:easymock:3.2',
+  sha1 = '00c82f7fa3ef377d8954b1db25123944b5af2ba4',
+  license = 'DO_NOT_DISTRIBUTE',
+  attach_source = False,
+  deps = [
+    ':cglib-2_2',
+    ':objenesis',
+  ],
+)
+
+maven_jar(
+  name = 'cglib-2_2',
+  id = 'cglib:cglib-nodep:2.2.2',
+  sha1 = '00d456bb230c70c0b95c76fb28e429d42f275941',
+  license = 'DO_NOT_DISTRIBUTE',
+  attach_source = False,
+)
+
+maven_jar(
+  name = 'objenesis',
+  id = 'org.objenesis:objenesis:1.2',
+  sha1 = 'bfcb0539a071a4c5a30690388903ac48c0667f2a',
+  license = 'DO_NOT_DISTRIBUTE',
+  visibility = ['//lib/powermock:powermock-reflect'],
+  attach_source = False,
+)
+
diff --git a/pom.xml b/pom.xml
index d0072e7..31cbbaa 100644
--- a/pom.xml
+++ b/pom.xml
@@ -80,6 +80,12 @@
       <version>4.11</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.easymock</groupId>
+      <artifactId>easymock</artifactId>
+      <version>3.2</version>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <repositories>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/FetchAndPushEventCreator.java b/src/main/java/com/googlesource/gerrit/plugins/quota/FetchAndPushEventCreator.java
new file mode 100644
index 0000000..fd41c4b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/FetchAndPushEventCreator.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.quota;
+
+import com.google.gerrit.extensions.events.UsageDataPublishedListener.Event;
+import com.google.gerrit.extensions.events.UsageDataPublishedListener.MetaData;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Singleton;
+
+@Singleton
+public class FetchAndPushEventCreator implements UsageDataEventCreator {
+
+  static final MetaData PUSH_COUNT = new MetaDataImpl("pushCount", "", "",
+      "number of pushes to the repository since the last event");
+
+  static final MetaData FETCH_COUNT = new MetaDataImpl("fetchCount", "", "",
+      "number of fetches from the repository since the last event");
+
+  private final ProjectCache projectCache;
+  private final PersistentCounter counts;
+  private final MetaData metaData;
+
+  public FetchAndPushEventCreator(ProjectCache projectCache, PersistentCounter counts,
+      MetaData metaData) {
+        this.projectCache = projectCache;
+        this.counts = counts;
+        this.metaData = metaData;
+  }
+
+  @Override
+  public String getName() {
+    return metaData.getName();
+  }
+
+  @Override
+  public Event create() {
+    UsageDataEvent event = new UsageDataEvent(metaData);
+    for (Project.NameKey p : projectCache.all()) {
+      long currentCount = counts.getAndReset(p);
+      if (currentCount != 0) {
+        event.addData(currentCount, p.get());
+      }
+    }
+    return event;
+  }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/Module.java b/src/main/java/com/googlesource/gerrit/plugins/quota/Module.java
index 6fbe150..1b0867a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/Module.java
@@ -28,7 +28,6 @@
 import org.eclipse.jgit.transport.PostReceiveHook;
 import org.eclipse.jgit.transport.PreUploadHook;
 
-
 class Module extends AbstractModule {
 
   @Override
@@ -43,6 +42,7 @@
         .to(FetchAndPushListener.class);
     DynamicSet.bind(binder(), PreUploadHook.class)
         .to(FetchAndPushListener.class);
+    DynamicSet.setOf(binder(), UsageDataEventCreator.class);
     install(MaxRepositorySizeQuota.module());
     install(PersistentCounter.module());
     install(new RestApiModule() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/PersistentCounter.java b/src/main/java/com/googlesource/gerrit/plugins/quota/PersistentCounter.java
index d99bcb5..b3c22df 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/PersistentCounter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/PersistentCounter.java
@@ -2,12 +2,17 @@
 
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Key;
 import com.google.inject.Module;
 import com.google.inject.Provides;
 import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
+import com.google.inject.name.Names;
 
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
@@ -23,6 +28,16 @@
             Loader.class).expireAfterWrite(Integer.MAX_VALUE, TimeUnit.DAYS);
         persist(PUSH, Project.NameKey.class, AtomicLong.class).loader(
             Loader.class).expireAfterWrite(Integer.MAX_VALUE, TimeUnit.DAYS);
+        DynamicSet.bind(binder(), UsageDataEventCreator.class).to(creatorKey(FETCH));
+        DynamicSet.bind(binder(), UsageDataEventCreator.class).to(creatorKey(PUSH));
+        DynamicSet.bind(binder(), UsageDataEventCreator.class).to(RepoSizeEventCreator.class);
+      }
+
+      private Key<UsageDataEventCreator> creatorKey(String kind) {
+        Key<UsageDataEventCreator> pushCreatorKey =
+            Key.get(new TypeLiteral<UsageDataEventCreator>() {},
+                Names.named(kind));
+        return pushCreatorKey;
       }
 
       @Provides @Singleton @Named(FETCH)
@@ -36,6 +51,18 @@
           @Named(PUSH) LoadingCache<Project.NameKey, AtomicLong> counts) {
         return new PersistentCounter(counts);
       }
+
+      @Provides @Singleton @Named(FETCH)
+      UsageDataEventCreator provideFetchEventCreator(ProjectCache projectCache,
+          @Named(FETCH) PersistentCounter counts) {
+        return new FetchAndPushEventCreator(projectCache, counts, FetchAndPushEventCreator.FETCH_COUNT);
+      }
+
+      @Provides @Singleton @Named(PUSH)
+      UsageDataEventCreator providePushEventCreator(ProjectCache projectCache,
+          @Named(PUSH) PersistentCounter counts) {
+        return new FetchAndPushEventCreator(projectCache, counts, FetchAndPushEventCreator.PUSH_COUNT);
+      }
     };
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/Publisher.java b/src/main/java/com/googlesource/gerrit/plugins/quota/Publisher.java
index 26637f8..f44c92a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/Publisher.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/Publisher.java
@@ -15,20 +15,14 @@
 package com.googlesource.gerrit.plugins.quota;
 
 import com.google.gerrit.extensions.events.UsageDataPublishedListener;
-import com.google.gerrit.extensions.events.UsageDataPublishedListener.Data;
 import com.google.gerrit.extensions.events.UsageDataPublishedListener.Event;
-import com.google.gerrit.extensions.events.UsageDataPublishedListener.MetaData;
 import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import com.google.inject.name.Named;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -36,33 +30,16 @@
 public class Publisher implements Runnable {
 
   private static final Logger log = LoggerFactory.getLogger(Publisher.class);
-  private static final MetaData REPO_SIZE = new MetaDataImpl("repoSize",
-      "total file size of the repository", "byte", "B");
-
-  private static final MetaData PUSH_COUNT = new MetaDataImpl("pushCount",
-      "number of pushes to the repository since the last event", "", "");
-
-  private static final MetaData FETCH_COUNT = new MetaDataImpl("fetchCount",
-      "number of fetches from the repository since the last event", "", "");
 
   private final Iterable<UsageDataPublishedListener> listeners;
-  private final ProjectCache projectCache;
-  private final RepoSizeCache repoSizeCache;
-  private final PersistentCounter fetchCounts;
-  private final PersistentCounter pushCounts;
+  private final DynamicSet<UsageDataEventCreator> creators;
 
   @Inject
   public Publisher(
       DynamicSet<UsageDataPublishedListener> listeners,
-      ProjectCache projectCache,
-      RepoSizeCache repoSizeCache,
-      @Named(PersistentCounter.FETCH) PersistentCounter fetchCounts,
-      @Named(PersistentCounter.PUSH) PersistentCounter pushCounts) {
+      DynamicSet<UsageDataEventCreator> creators) {
     this.listeners = listeners;
-    this.projectCache = projectCache;
-    this.repoSizeCache = repoSizeCache;
-    this.fetchCounts = fetchCounts;
-    this.pushCounts = pushCounts;
+    this.creators = creators;
   }
 
   @Override
@@ -71,84 +48,25 @@
       return;
     }
 
-    UsageDataEvent repoSizeEvent = createRepoSizeEvent();
-    UsageDataEvent fetchCountEvent = createEvent(FETCH_COUNT, fetchCounts);
-    UsageDataEvent pushCountEvent = createEvent(PUSH_COUNT, pushCounts);
+    List<UsageDataPublishedListener.Event> events = new ArrayList<UsageDataPublishedListener.Event>(3);
+    for (UsageDataEventCreator creator : creators) {
+      try {
+        events.add(creator.create());
+      } catch (RuntimeException e) {
+        String creatorName = creator.getName();
+        log.warn("Exception in usage data event creator " + creatorName, e);
+      }
+    }
+
     for (UsageDataPublishedListener l : listeners) {
       try {
-        l.onUsageDataPublished(repoSizeEvent);
-        l.onUsageDataPublished(pushCountEvent);
-        l.onUsageDataPublished(fetchCountEvent);
+        for (Event event : events) {
+          l.onUsageDataPublished(event);
+        }
       } catch (RuntimeException e) {
-        log.warn("Failure in UsageDataPublishedListener", e);
+        log.warn("Exception in UsageDataPublishedListener", e);
       }
     }
   }
 
-  private UsageDataEvent createRepoSizeEvent() {
-    UsageDataEvent event = new UsageDataEvent(REPO_SIZE);
-    for (Project.NameKey p : projectCache.all()) {
-      long size = repoSizeCache.get(p);
-      if (size > 0) {
-        event.addData(size, p.get());
-      }
-    }
-    return event;
-  }
-
-  private UsageDataEvent createEvent(MetaData metaData, PersistentCounter counts) {
-    UsageDataEvent event = new UsageDataEvent(metaData);
-    for (Project.NameKey p : projectCache.all()) {
-      long currentCount = counts.getAndReset(p);
-      if (currentCount != 0) {
-        event.addData(currentCount, p.get());
-      }
-    }
-    return event;
-  }
-
-  private static class UsageDataEvent implements Event {
-
-    private final Timestamp timestamp;
-    private final MetaData metaData;
-    private final List<Data> data;
-
-    public UsageDataEvent(MetaData metaData) {
-      this.metaData = metaData;
-      timestamp = new Timestamp(System.currentTimeMillis());
-      data = new ArrayList<Data>();
-    }
-
-    private void addData(final long value, final String projectName) {
-      Data dataRow = new Data() {
-
-        @Override
-        public long getValue() {
-          return value;
-        }
-
-        @Override
-        public String getProjectName() {
-          return projectName;
-        }
-      };
-
-      data.add(dataRow);
-    }
-
-    @Override
-    public MetaData getMetaData() {
-      return metaData;
-    }
-
-    @Override
-    public Timestamp getInstant() {
-      return timestamp  ;
-    }
-
-    @Override
-    public List<Data> getData() {
-      return data;
-    }
-  };
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/RepoSizeEventCreator.java b/src/main/java/com/googlesource/gerrit/plugins/quota/RepoSizeEventCreator.java
new file mode 100644
index 0000000..8293ebe
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/RepoSizeEventCreator.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.quota;
+
+import com.google.gerrit.extensions.events.UsageDataPublishedListener.Event;
+import com.google.gerrit.extensions.events.UsageDataPublishedListener.MetaData;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class RepoSizeEventCreator implements UsageDataEventCreator {
+
+  private static final MetaData REPO_SIZE = new MetaDataImpl("repoSize", "byte", "B",
+      "total file size of the repository");
+
+  private final ProjectCache projectCache;
+  private final RepoSizeCache repoSizeCache;
+
+  @Inject
+  public RepoSizeEventCreator(ProjectCache projectCache,
+      RepoSizeCache repoSizeCache) {
+    this.projectCache = projectCache;
+    this.repoSizeCache = repoSizeCache;
+  }
+
+  private UsageDataEvent createRepoSizeEvent() {
+    UsageDataEvent event = new UsageDataEvent(REPO_SIZE);
+    for (Project.NameKey p : projectCache.all()) {
+      long size = repoSizeCache.get(p);
+      if (size > 0) {
+        event.addData(size, p.get());
+      }
+    }
+    return event;
+  }
+
+  @Override
+  public String getName() {
+    return REPO_SIZE.getName();
+  };
+
+  @Override
+  public Event create() {
+    return createRepoSizeEvent();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/UsageDataEvent.java b/src/main/java/com/googlesource/gerrit/plugins/quota/UsageDataEvent.java
new file mode 100644
index 0000000..c596b09
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/UsageDataEvent.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.quota;
+
+import com.google.gerrit.extensions.events.UsageDataPublishedListener.Data;
+import com.google.gerrit.extensions.events.UsageDataPublishedListener.Event;
+import com.google.gerrit.extensions.events.UsageDataPublishedListener.MetaData;
+
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.List;
+
+class UsageDataEvent implements Event {
+
+  private final Timestamp timestamp;
+  private final MetaData metaData;
+  private final List<Data> data;
+
+  public UsageDataEvent(MetaData metaData) {
+    this.metaData = metaData;
+    timestamp = new Timestamp(System.currentTimeMillis());
+    data = new ArrayList<Data>();
+  }
+
+  void addData(final long value, final String projectName) {
+    Data dataRow = new Data() {
+
+      @Override
+      public long getValue() {
+        return value;
+      }
+
+      @Override
+      public String getProjectName() {
+        return projectName;
+      }
+    };
+
+    data.add(dataRow);
+  }
+
+  @Override
+  public MetaData getMetaData() {
+    return metaData;
+  }
+
+  @Override
+  public Timestamp getInstant() {
+    return timestamp  ;
+  }
+
+  @Override
+  public List<Data> getData() {
+    return data;
+  }
+}
\ No newline at end of file
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/UsageDataEventCreator.java b/src/main/java/com/googlesource/gerrit/plugins/quota/UsageDataEventCreator.java
new file mode 100644
index 0000000..5ad42b9
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/UsageDataEventCreator.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.quota;
+
+import com.google.gerrit.extensions.events.UsageDataPublishedListener;
+
+public interface UsageDataEventCreator {
+
+  public String getName();
+
+  public UsageDataPublishedListener.Event create();
+
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/quota/PublisherExceptionTest.java b/src/test/java/com/googlesource/gerrit/plugins/quota/PublisherExceptionTest.java
new file mode 100644
index 0000000..dbe03f8
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/quota/PublisherExceptionTest.java
@@ -0,0 +1,143 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.quota;
+
+import static org.easymock.EasyMock.*;
+import static org.junit.Assert.*;
+
+import com.google.gerrit.extensions.events.UsageDataPublishedListener;
+import com.google.gerrit.extensions.events.UsageDataPublishedListener.Event;
+import com.google.gerrit.extensions.registration.DynamicSet;
+
+import org.apache.log4j.Appender;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.spi.LoggingEvent;
+import org.easymock.Capture;
+import org.junit.Before;
+import org.junit.Test;
+
+public class PublisherExceptionTest {
+
+  private static final String CREATOR_NAME = "test-creator";
+  private UsageDataPublishedListener listener;
+  private UsageDataEventCreator creator;
+  private Publisher classUnderTest;
+  private Appender appender;
+  private Capture<LoggingEvent> captor;
+  private DynamicSet<UsageDataPublishedListener> listeners;
+  private DynamicSet<UsageDataEventCreator> creators;
+
+  @Before
+  public void setupClassUnderTest() {
+    listener = createMock(UsageDataPublishedListener.class);
+    listeners = DynamicSet.emptySet();
+    listeners.add(listener);
+
+    creator = createMock(UsageDataEventCreator.class);
+    expect(creator.getName()).andStubReturn(CREATOR_NAME);
+    creators = DynamicSet.emptySet();
+    creators.add(creator);
+
+    classUnderTest = new Publisher(listeners, creators);
+  }
+
+  @Before
+  public void setupLogging() {
+    captor = new Capture<LoggingEvent>();
+    appender = createMock(Appender.class);
+    appender.doAppend(capture(captor));
+    expectLastCall().anyTimes();
+    Logger.getRootLogger().addAppender(appender);
+  }
+
+  @Test
+  public void testExceptionInCreatorIsLogged() {
+    RuntimeException ex = new RuntimeException();
+    expect(creator.create()).andStubThrow(ex);
+
+    replay(listener, creator, appender);
+
+    classUnderTest.run();
+
+    verify(listener, creator, appender);
+
+    assertTrue(captor.hasCaptured());
+    LoggingEvent event = captor.getValue();
+    assertEquals(Level.WARN, event.getLevel());
+    assertTrue(((String)event.getMessage()).contains(CREATOR_NAME));
+  }
+
+  @Test
+  public void testDataFromGoodCreatorIsPropagated() {
+    RuntimeException ex = new RuntimeException();
+    expect(creator.create()).andStubThrow(ex);
+
+    UsageDataEventCreator good = createMock(UsageDataEventCreator.class);
+    Event data = new UsageDataEvent(FetchAndPushEventCreator.FETCH_COUNT);
+    expect(good.create()).andStubReturn(data);
+    creators.add(good);
+
+    listener.onUsageDataPublished(data);
+    expectLastCall();
+
+    replay(listener, creator, good, appender);
+
+    classUnderTest.run();
+
+    verify(listener, creator, appender);
+  }
+
+  @Test
+  public void testExceptionInListenerIsLogged() {
+    RuntimeException ex = new RuntimeException();
+    Event data = new UsageDataEvent(FetchAndPushEventCreator.FETCH_COUNT);
+    expect(creator.create()).andStubReturn(data);
+
+    listener.onUsageDataPublished(data);
+    expectLastCall().andStubThrow(ex);
+
+    replay(listener, creator, appender);
+
+    classUnderTest.run();
+
+    verify(listener, creator, appender);
+
+    assertTrue(captor.hasCaptured());
+    LoggingEvent event = captor.getValue();
+    assertEquals(Level.WARN, event.getLevel());
+  }
+
+  @Test
+  public void testIsPropagatedToGoodListener() {
+    RuntimeException ex = new RuntimeException();
+    Event data = new UsageDataEvent(FetchAndPushEventCreator.FETCH_COUNT);
+    expect(creator.create()).andStubReturn(data);
+
+    listener.onUsageDataPublished(data);
+    expectLastCall().andStubThrow(ex);
+
+    UsageDataPublishedListener good = createMock(UsageDataPublishedListener.class);
+    good.onUsageDataPublished(data);
+    listeners.add(good);
+
+    replay(listener, good, creator, appender);
+
+    classUnderTest.run();
+
+    verify(listener, good, creator, appender);
+  }
+
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/quota/PublisherTest.java b/src/test/java/com/googlesource/gerrit/plugins/quota/PublisherTest.java
new file mode 100644
index 0000000..077b056
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/quota/PublisherTest.java
@@ -0,0 +1,106 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.quota;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+
+import com.google.gerrit.extensions.events.UsageDataPublishedListener;
+import com.google.gerrit.extensions.events.UsageDataPublishedListener.Event;
+import com.google.gerrit.extensions.registration.DynamicSet;
+
+import org.junit.Test;
+
+public class PublisherTest {
+
+  @Test
+  public void testAllEventsPropagatedToListener() throws Exception {
+    Event e1 = new UsageDataEvent(FetchAndPushEventCreator.FETCH_COUNT);
+    UsageDataEventCreator c1 = createMock(UsageDataEventCreator.class);
+    expect(c1.create()).andStubReturn(e1);
+
+    Event e2 = new UsageDataEvent(FetchAndPushEventCreator.PUSH_COUNT);
+    UsageDataEventCreator c2 = createMock(UsageDataEventCreator.class);
+    expect(c2.create()).andStubReturn(e2);
+
+    DynamicSet<UsageDataEventCreator> creators = DynamicSet.emptySet();
+    creators.add(c1);
+    creators.add(c2);
+
+    UsageDataPublishedListener listener =
+        createMock(UsageDataPublishedListener.class);
+    listener.onUsageDataPublished(e1);
+    expectLastCall();
+    listener.onUsageDataPublished(e2);
+    expectLastCall();
+
+    replay(c1, c2, listener);
+    DynamicSet<UsageDataPublishedListener> listeners = DynamicSet.emptySet();
+    listeners.add(listener);
+
+    Publisher classUnderTest = new Publisher(listeners, creators);
+    classUnderTest.run();
+
+    verify(c1, c2, listener);
+  }
+
+  @Test
+  public void testEventPropagatedToAllListeners() throws Exception {
+    Event event = new UsageDataEvent(FetchAndPushEventCreator.FETCH_COUNT);
+    UsageDataEventCreator creator = createMock(UsageDataEventCreator.class);
+    expect(creator.create()).andStubReturn(event);
+    DynamicSet<UsageDataEventCreator> creators = DynamicSet.emptySet();
+    creators.add(creator);
+
+    UsageDataPublishedListener l1 =
+        createMock(UsageDataPublishedListener.class);
+    l1.onUsageDataPublished(event);
+    expectLastCall();
+
+    UsageDataPublishedListener l2 =
+        createMock(UsageDataPublishedListener.class);
+    l2.onUsageDataPublished(event);
+    expectLastCall();
+
+    replay(creator, l1, l2);
+
+    DynamicSet<UsageDataPublishedListener> listeners = DynamicSet.emptySet();
+    listeners.add(l1);
+    listeners.add(l2);
+
+    Publisher classUnderTest = new Publisher(listeners, creators);
+    classUnderTest.run();
+
+    verify(creator, l1, l2);
+  }
+
+  @Test
+  public void testNoEventsCreatedIfNoListenersRegistered() throws Exception {
+    UsageDataEventCreator creator = createMock(UsageDataEventCreator.class);
+    replay(creator);
+    DynamicSet<UsageDataEventCreator> creators = DynamicSet.emptySet();
+    creators.add(creator);
+
+    DynamicSet<UsageDataPublishedListener> listeners = DynamicSet.emptySet();
+    Publisher classUnderTest = new Publisher(listeners, creators);
+    classUnderTest.run();
+
+    verify(creator);
+  }
+
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/quota/RepoSizeEventCreatorTest.java b/src/test/java/com/googlesource/gerrit/plugins/quota/RepoSizeEventCreatorTest.java
new file mode 100644
index 0000000..f67f776
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/quota/RepoSizeEventCreatorTest.java
@@ -0,0 +1,90 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.quota;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.extensions.events.UsageDataPublishedListener.Data;
+import com.google.gerrit.extensions.events.UsageDataPublishedListener.Event;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gwtorm.client.KeyUtil;
+import com.google.gwtorm.server.StandardKeyEncoder;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+
+public class RepoSizeEventCreatorTest {
+
+  static {
+    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
+  }
+
+  private Project.NameKey p1 = new Project.NameKey("p1");
+  private Project.NameKey p2 = new Project.NameKey("p2");
+  private Project.NameKey p3 = new Project.NameKey("p3");
+  private ProjectCache projectCache;
+  private RepoSizeEventCreator classUnderTest;
+  private File tmp;
+  private RepoSizeCache repoSizeCache;
+
+  @Before
+  public void setup() throws IOException {
+    tmp = File.createTempFile("quota-test", "dir");
+    tmp.delete();
+    tmp.mkdir();
+    projectCache = createMock(ProjectCache.class);
+    Iterable<Project.NameKey> projects = Arrays.asList(p1, p2, p3);
+    expect(projectCache.all()).andStubReturn(projects);
+    repoSizeCache = createNiceMock(RepoSizeCache.class);
+    replay(projectCache);
+    classUnderTest = new RepoSizeEventCreator(projectCache, repoSizeCache);
+  }
+
+
+  @Test
+  public void testEmpty() {
+    replay(repoSizeCache);
+
+    Event event = classUnderTest.create();
+
+    assertEquals("repoSize", event.getMetaData().getName());
+    assertTrue(event.getData().isEmpty());
+  }
+
+  @Test
+  public void testOneDataPoint() {
+    expect(repoSizeCache.get(p1)).andStubReturn(100l);
+    replay(repoSizeCache);
+
+    Event event = classUnderTest.create();
+
+    assertEquals("repoSize", event.getMetaData().getName());
+    assertEquals(1, event.getData().size());
+    Data dataPoint = event.getData().get(0);
+    assertEquals("p1", dataPoint.getProjectName());
+    assertEquals(100l, dataPoint.getValue());
+  }
+
+}