/*
 * Copyright (C) 2009 Google Inc.
 *
 * 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.google.caliper.runner;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import com.google.caliper.Benchmark;
import com.google.caliper.Param;
import com.google.caliper.core.InvalidBenchmarkException;
import com.google.caliper.runner.config.InvalidConfigurationException;
import com.google.caliper.util.InvalidCommandException;
import java.io.PrintWriter;
import java.io.StringWriter;
import junit.framework.AssertionFailedError;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Unit test covering common user mistakes in benchmark classes. */
@RunWith(JUnit4.class)

public class MalformedBenchmarksTest {
  // Put the expected messages together here, which may promote some kind of
  // consistency in their wording. :)

  private static final String ABSTRACT = "Class '%s' is abstract";
  private static final String NO_CONSTRUCTOR =
      "Benchmark class '%s' does not have a publicly visible default constructor";
  private static final String NO_METHODS =
      "There were no experiments to be performed for the class '%s' using the instruments "
          + "[allocation, runtime]";
  private static final String STATIC_BENCHMARK = "Benchmark methods must not be static: timeIt";
  private static final String WRONG_ARGUMENTS =
      "Benchmark methods must have no arguments or accept a single int or long parameter: timeIt";
  private static final String STATIC_PARAM = "Parameter field 'oops' must not be static";
  private static final String RESERVED_PARAM = "Class '%s' uses reserved parameter name 'vm'";
  private static final String NO_CONVERSION =
      "Type 'java.lang.Object' of parameter field 'oops' "
          + "has no recognized String-converting method; see <TODO> for details";
  private static final String CONVERT_FAILED = // granted this one's a little weird (and brittle)
      "Cannot convert value 'oops' to type 'int': For input string: \"oops\"";

  @Test
  public void abstractBenchmark() throws Exception {
    expectException(ABSTRACT, AbstractBenchmark.class);
  }

  abstract static class AbstractBenchmark {}

  @Test
  public void noSuitableConstructor() throws Exception {
    expectException(
        String.format(NO_CONSTRUCTOR, BadConstructorBenchmark.class.getName()),
        BadConstructorBenchmark.class);
  }

  @SuppressWarnings("unused")
  static class BadConstructorBenchmark {
    BadConstructorBenchmark(String damnParam) {}

    @Benchmark
    void timeIt(int reps) {}
  }

  @Test
  public void noBenchmarkMethods() throws Exception {
    expectException(NO_METHODS, NoMethodsBenchmark.class);
  }

  @SuppressWarnings("unused")
  static class NoMethodsBenchmark {
    void timeIt(int reps) {} // not annotated
  }

  @Test
  public void staticBenchmarkMethod() throws Exception {
    expectException(STATIC_BENCHMARK, StaticBenchmarkMethodBenchmark.class);
  }

  @SuppressWarnings("unused")
  static class StaticBenchmarkMethodBenchmark {
    @Benchmark
    public static void timeIt(int reps) {}
  }

  @Test
  public void wrongSignature() throws Exception {
    expectException(WRONG_ARGUMENTS, BoxedParamBenchmark.class);
    expectException(WRONG_ARGUMENTS, ExtraParamBenchmark.class);
  }

  @SuppressWarnings("unused")
  static class BoxedParamBenchmark {
    @Benchmark
    void timeIt(Integer reps) {}
  }

  @SuppressWarnings("unused")
  static class ExtraParamBenchmark {
    @Benchmark
    void timeIt(int reps, int what) {}
  }

  @Test
  public void hasBenchmarkOverloads() throws Exception {
    // N.B. baz is fine since although it has an overload, its overload is not a benchmark method.
    expectException(
        "Overloads are disallowed for benchmark methods, found overloads of [bar, foo] in "
            + "benchmark OverloadsAnnotatedBenchmark",
        OverloadsAnnotatedBenchmark.class);
  }

  @SuppressWarnings("unused")
  static class OverloadsAnnotatedBenchmark {
    @Benchmark
    public void foo(long reps) {}

    @Benchmark
    public void foo(int reps) {}

    @Benchmark
    public void bar(long reps) {}

    @Benchmark
    public void bar(int reps) {}

    @Benchmark
    public void baz(int reps) {}

    public void baz(long reps, boolean thing) {}

    public void baz(long reps) {}
  }

  @Test
  public void staticParam() throws Exception {
    expectException(STATIC_PARAM, StaticParamBenchmark.class);
  }

  static class StaticParamBenchmark {
    @Param static String oops;
  }

  @Test
  public void reservedParameterName() throws Exception {
    expectException(RESERVED_PARAM, ReservedParamBenchmark.class);
  }

  static class ReservedParamBenchmark {
    @Param String vm;
  }

  @Test
  public void unparsableParamType() throws Exception {
    expectException(NO_CONVERSION, UnparsableParamTypeBenchmark.class);
  }

  static class UnparsableParamTypeBenchmark {
    @Param Object oops;
  }

  @Test
  public void unparsableParamDefault() throws Exception {
    expectException(CONVERT_FAILED, UnparsableParamDefaultBenchmark.class);
  }

  static class UnparsableParamDefaultBenchmark {
    @Param({"1", "2", "oops"})
    int number;
  }

  // end of tests

  private void expectException(String expectedMessageFmt, Class<?> benchmarkClass)
      throws InvalidCommandException, InvalidConfigurationException {
    try {
      CaliperMain.exitlessMain(
          new String[] {"--instrument=allocation,runtime", "--dry-run", benchmarkClass.getName()},
          new PrintWriter(new StringWriter()),
          new PrintWriter(new StringWriter()));
      fail("no exception thrown");
    } catch (InvalidBenchmarkException e) {
      try {
        String expectedMessageText = String.format(expectedMessageFmt, benchmarkClass.getName());
        assertEquals(expectedMessageText, e.getMessage());

        // don't swallow our real stack trace
      } catch (AssertionFailedError afe) {
        afe.initCause(e);
        throw afe;
      }
    }
  }
}
