Home | History | Annotate | Download | only in runner
      1 /*
      2  * Copyright (C) 2011 Google Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.google.caliper.runner;
     18 
     19 import static com.google.common.base.Preconditions.checkNotNull;
     20 import static com.google.common.base.Throwables.propagateIfInstanceOf;
     21 
     22 import com.google.caliper.AfterExperiment;
     23 import com.google.caliper.BeforeExperiment;
     24 import com.google.caliper.Param;
     25 import com.google.caliper.api.SkipThisScenarioException;
     26 import com.google.caliper.api.VmOptions;
     27 import com.google.caliper.util.InvalidCommandException;
     28 import com.google.caliper.util.Reflection;
     29 import com.google.common.annotations.VisibleForTesting;
     30 import com.google.common.base.Objects;
     31 import com.google.common.collect.ImmutableSet;
     32 import com.google.common.collect.ImmutableSetMultimap;
     33 
     34 import java.lang.reflect.InvocationTargetException;
     35 import java.lang.reflect.Method;
     36 import java.lang.reflect.Modifier;
     37 import java.util.logging.Level;
     38 import java.util.logging.Logger;
     39 
     40 /**
     41  * An instance of this type represents a user-provided class. It manages creating, setting up and
     42  * destroying instances of that class.
     43  */
     44 final class BenchmarkClass {
     45   private static final Logger logger = Logger.getLogger(BenchmarkClass.class.getName());
     46 
     47   static BenchmarkClass forClass(Class<?> theClass) throws InvalidBenchmarkException {
     48     return new BenchmarkClass(theClass);
     49   }
     50 
     51   final Class<?> theClass;
     52   private final ParameterSet userParameters;
     53   private final ImmutableSet<String> benchmarkFlags;
     54 
     55   private BenchmarkClass(Class<?> theClass) throws InvalidBenchmarkException {
     56     this.theClass = checkNotNull(theClass);
     57 
     58     if (!theClass.getSuperclass().equals(Object.class)) {
     59       throw new InvalidBenchmarkException(
     60           "%s must not extend any class other than %s. Prefer composition.",
     61           theClass, Object.class);
     62     }
     63 
     64     if (Modifier.isAbstract(theClass.getModifiers())) {
     65       throw new InvalidBenchmarkException("Class '%s' is abstract", theClass);
     66     }
     67 
     68     // TODO: check for nested, non-static classes (non-abstract, but no constructor?)
     69     // this will fail later anyway (no way to declare parameterless nested constr., but
     70     // maybe signal this better?
     71 
     72     this.userParameters = ParameterSet.create(theClass, Param.class);
     73 
     74     this.benchmarkFlags = getVmOptions(theClass);
     75   }
     76 
     77   ImmutableSet<Method> beforeExperimentMethods() {
     78     return Reflection.getAnnotatedMethods(theClass, BeforeExperiment.class);
     79   }
     80 
     81   ImmutableSet<Method> afterExperimentMethods() {
     82     return Reflection.getAnnotatedMethods(theClass, AfterExperiment.class);
     83   }
     84 
     85   public ParameterSet userParameters() {
     86     return userParameters;
     87   }
     88 
     89   public ImmutableSet<String> vmOptions() {
     90     return benchmarkFlags;
     91   }
     92 
     93   // TODO(gak): use these methods in the worker as well
     94   public void setUpBenchmark(Object benchmarkInstance) throws UserCodeException {
     95     boolean setupSuccess = false;
     96     try {
     97       callSetUp(benchmarkInstance);
     98       setupSuccess = true;
     99     } finally {
    100       // If setUp fails, we should call tearDown. If this method throws an exception, we
    101       // need to call tearDown from here, because no one else has the reference to the
    102       // Benchmark.
    103       if (!setupSuccess) {
    104         try {
    105           callTearDown(benchmarkInstance);
    106         } catch (UserCodeException e) {
    107           // The exception thrown during setUp shouldn't be lost, as it's probably more
    108           // important to the user.
    109           logger.log(
    110               Level.INFO,
    111               "in @AfterExperiment methods called because @BeforeExperiment methods failed",
    112               e);
    113         }
    114       }
    115     }
    116   }
    117 
    118   public void cleanup(Object benchmark) throws UserCodeException {
    119     callTearDown(benchmark);
    120   }
    121 
    122   @VisibleForTesting Class<?> benchmarkClass() {
    123     return theClass;
    124   }
    125 
    126   public String name() {
    127     return theClass.getName();
    128   }
    129 
    130   @Override public boolean equals(Object obj) {
    131     if (obj == this) {
    132       return true;
    133     } else if (obj instanceof BenchmarkClass) {
    134       BenchmarkClass that = (BenchmarkClass) obj;
    135       return this.theClass.equals(that.theClass);
    136     } else {
    137       return false;
    138     }
    139   }
    140 
    141   @Override public int hashCode() {
    142     return Objects.hashCode(theClass);
    143   }
    144 
    145   @Override public String toString() {
    146     return name();
    147   }
    148 
    149   private void callSetUp(Object benchmark) throws UserCodeException {
    150     for (Method method : beforeExperimentMethods()) {
    151       try {
    152         method.invoke(benchmark);
    153       } catch (IllegalAccessException e) {
    154         throw new AssertionError(e);
    155       } catch (InvocationTargetException e) {
    156         propagateIfInstanceOf(e.getCause(), SkipThisScenarioException.class);
    157         throw new UserCodeException(
    158             "Exception thrown from a @BeforeExperiment method", e.getCause());
    159       }
    160     }
    161   }
    162 
    163   private void callTearDown(Object benchmark) throws UserCodeException {
    164     for (Method method : afterExperimentMethods()) {
    165       try {
    166         method.invoke(benchmark);
    167       } catch (IllegalAccessException e) {
    168         throw new AssertionError(e);
    169       } catch (InvocationTargetException e) {
    170         propagateIfInstanceOf(e.getCause(), SkipThisScenarioException.class);
    171         throw new UserCodeException(
    172             "Exception thrown from an @AfterExperiment method", e.getCause());
    173       }
    174     }
    175   }
    176 
    177   private static ImmutableSet<String> getVmOptions(Class<?> benchmarkClass) {
    178     VmOptions annotation = benchmarkClass.getAnnotation(VmOptions.class);
    179     return (annotation == null)
    180         ? ImmutableSet.<String>of()
    181         : ImmutableSet.copyOf(annotation.value());
    182   }
    183 
    184   void validateParameters(ImmutableSetMultimap<String, String> parameters)
    185       throws InvalidCommandException {
    186     for (String paramName : parameters.keySet()) {
    187       Parameter parameter = userParameters.get(paramName);
    188       if (parameter == null) {
    189         throw new InvalidCommandException("unrecognized parameter: " + paramName);
    190       }
    191       try {
    192         parameter.validate(parameters.get(paramName));
    193       } catch (InvalidBenchmarkException e) {
    194         // TODO(kevinb): this is weird.
    195         throw new InvalidCommandException(e.getMessage());
    196       }
    197     }
    198   }
    199 }
    200