You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ho...@apache.org on 2016/06/01 21:34:40 UTC

lucene-solr:master: SOLR-9107: new @RandomizeSSL annotation for more fine grained control of SSL testing

Repository: lucene-solr
Updated Branches:
  refs/heads/master 7de3ef351 -> 09372acb6


SOLR-9107: new @RandomizeSSL annotation for more fine grained control of SSL testing


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/09372acb
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/09372acb
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/09372acb

Branch: refs/heads/master
Commit: 09372acb660d21b6da01f6ea65f00493126ee32b
Parents: 7de3ef3
Author: Chris Hostetter <ho...@apache.org>
Authored: Wed Jun 1 14:34:31 2016 -0700
Committer: Chris Hostetter <ho...@apache.org>
Committed: Wed Jun 1 14:34:31 2016 -0700

----------------------------------------------------------------------
 solr/CHANGES.txt                                |   2 +
 .../apache/solr/cloud/TestSSLRandomization.java | 202 +++++++++++++++++++
 .../java/org/apache/solr/SolrTestCaseJ4.java    |  27 ++-
 .../java/org/apache/solr/util/RandomizeSSL.java | 174 ++++++++++++++++
 4 files changed, 390 insertions(+), 15 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/09372acb/solr/CHANGES.txt
----------------------------------------------------------------------
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index d0a0cb8..c8fa3ff 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -347,6 +347,8 @@ Other Changes
 * SOLR-9136: Separate out the error statistics into server-side error vs client-side error
   (Jessica Cheng Mallet via Erick Erickson)
 
+* SOLR-9107: new @RandomizeSSL annotation for more fine grained control of SSL testing (hossman, sarowe)
+
 ==================  6.0.1 ==================
 (No Changes)
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/09372acb/solr/core/src/test/org/apache/solr/cloud/TestSSLRandomization.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/cloud/TestSSLRandomization.java b/solr/core/src/test/org/apache/solr/cloud/TestSSLRandomization.java
index e6dd90e..0c20a49 100644
--- a/solr/core/src/test/org/apache/solr/cloud/TestSSLRandomization.java
+++ b/solr/core/src/test/org/apache/solr/cloud/TestSSLRandomization.java
@@ -17,9 +17,13 @@
 package org.apache.solr.cloud;
 
 import java.lang.invoke.MethodHandles;
+import java.util.Arrays;
 
 import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.SolrTestCaseJ4.SuppressSSL;
 import org.apache.solr.util.SSLTestConfig;
+import org.apache.solr.util.RandomizeSSL;
+import org.apache.solr.util.RandomizeSSL.SSLRandomizer;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
@@ -34,6 +38,7 @@ import org.slf4j.LoggerFactory;
  *
  * @see TestMiniSolrCloudClusterSSL
  */
+@RandomizeSSL(ssl=0.5,reason="frequent SSL usage to make test worth while")
 public class TestSSLRandomization extends SolrCloudTestCase {
 
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@@ -51,4 +56,201 @@ public class TestSSLRandomization extends SolrCloudTestCase {
     String url = buildUrl(6666, "/foo");
     assertEquals(sslConfig.isSSLMode() ? "https://127.0.0.1:6666/foo" : "http://127.0.0.1:6666/foo", url);
   }
+  
+  /** Used by {@link #testSSLRandomizer} */
+  @RandomizeSSL(ssl=0.42,clientAuth=0.33,reason="foo")
+  public class FullyAnnotated { };
+  
+  /** Used by {@link #testSSLRandomizer} */
+  public class InheritedFullyAnnotated extends FullyAnnotated { };
+  
+  /** Used by {@link #testSSLRandomizer} */
+  public class NotAnnotated { };
+  
+  /** Used by {@link #testSSLRandomizer} */
+  public class InheritedNotAnnotated extends NotAnnotated { };
+  
+  /** Used by {@link #testSSLRandomizer} */
+  @SuppressSSL(bugUrl="fakeBugUrl")
+  public class Suppressed { };
+  
+  /** Used by {@link #testSSLRandomizer} */
+  public class InheritedSuppressed extends Suppressed { };
+  
+  /** Used by {@link #testSSLRandomizer} */
+  @SuppressSSL(bugUrl="fakeBugUrl")
+  public class InheritedAnnotationButSuppressed extends FullyAnnotated { };
+  
+  /** Used by {@link #testSSLRandomizer} */
+  @RandomizeSSL(ssl=0.42,clientAuth=0.33,reason="foo")
+  public class InheritedSuppressedWithIgnoredAnnotation extends Suppressed {
+    // Even with direct annotation, supression at superclass overrules us.
+    //
+    // (If it didn't work this way, it would be a pain in the ass to quickly disable SSL for a
+    // broad hierarchy of tests)
+  };
+  
+  /** Used by {@link #testSSLRandomizer} */
+  @RandomizeSSL()
+  public class EmptyAnnotated { };
+  
+  /** Used by {@link #testSSLRandomizer} */
+  public class InheritedEmptyAnnotated extends EmptyAnnotated { };
+  
+  /** Used by {@link #testSSLRandomizer} */
+  @RandomizeSSL(0.5)
+  public class InheritedEmptyAnnotatationWithOverride extends EmptyAnnotated { };
+  
+  /** Used by {@link #testSSLRandomizer} */
+  @RandomizeSSL(ssl=0.42,clientAuth=0.33,reason="foo")
+  public class GrandchildInheritedEmptyAnnotatationWithOverride extends InheritedEmptyAnnotated { };
+  
+  /** Used by {@link #testSSLRandomizer} */
+  @RandomizeSSL(0.5)
+  public class SimplyAnnotated { };
+  
+  /** Used by {@link #testSSLRandomizer} */
+  @RandomizeSSL(0.0)
+  public class MinAnnotated { };
+  
+  /** Used by {@link #testSSLRandomizer} */
+  @RandomizeSSL(1)
+  public class MaxAnnotated { };
+  
+  /** Used by {@link #testSSLRandomizer} */
+  @RandomizeSSL(ssl=0.42)
+  public class SSlButNoClientAuthAnnotated { };
+  
+  /** Used by {@link #testSSLRandomizer} */
+  @RandomizeSSL(clientAuth=0.42)
+  public class ClientAuthButNoSSLAnnotated { };
+  
+  /** Used by {@link #testSSLRandomizer} */
+  @RandomizeSSL(ssl=42.0)
+  public class SSLOutOfRangeAnnotated { };
+  
+  /** Used by {@link #testSSLRandomizer} */
+  @RandomizeSSL(clientAuth=42.0)
+  public class ClientAuthOutOfRangeAnnotated { };
+  
+  /** Used by {@link #testSSLRandomizer} */
+  public class InheritedOutOfRangeAnnotated extends ClientAuthOutOfRangeAnnotated { };
+  
+  public void testSSLRandomizer() {
+    SSLRandomizer r;
+    // for some cases, we know exactly what the config should be regardless of randomization factors
+    SSLTestConfig conf;
+
+    for (Class c : Arrays.asList(FullyAnnotated.class, InheritedFullyAnnotated.class,
+                                 GrandchildInheritedEmptyAnnotatationWithOverride.class )) {
+      r = SSLRandomizer.getSSLRandomizerForClass(c);
+      assertEquals(c.toString(), 0.42D, r.ssl, 0.0D);
+      assertEquals(c.toString(), 0.33D, r.clientAuth, 0.0D);
+      assertTrue(c.toString(), r.debug.contains("foo"));
+    }
+
+    for (Class c : Arrays.asList(NotAnnotated.class, InheritedNotAnnotated.class)) { 
+      r = SSLRandomizer.getSSLRandomizerForClass(c);
+      assertEquals(c.toString(), 0.0D, r.ssl, 0.0D);
+      assertEquals(c.toString(), 0.0D, r.clientAuth, 0.0D);
+      assertTrue(c.toString(), r.debug.contains("not specified"));
+      conf = r.createSSLTestConfig();
+      assertEquals(c.toString(), false, conf.isSSLMode());
+      assertEquals(c.toString(), false, conf.isClientAuthMode());
+    }
+
+    for (Class c : Arrays.asList(Suppressed.class,
+                                 InheritedSuppressed.class,
+                                 InheritedAnnotationButSuppressed.class,
+                                 InheritedSuppressedWithIgnoredAnnotation.class)) {
+      r = SSLRandomizer.getSSLRandomizerForClass(Suppressed.class);
+      assertEquals(c.toString(), 0.0D, r.ssl, 0.0D);
+      assertEquals(c.toString(), 0.0D, r.clientAuth, 0.0D);
+      assertTrue(c.toString(), r.debug.contains("SuppressSSL"));
+      assertTrue(c.toString(), r.debug.contains("fakeBugUrl"));
+      conf = r.createSSLTestConfig();
+      assertEquals(c.toString(), false, conf.isSSLMode());
+      assertEquals(c.toString(), false, conf.isClientAuthMode());
+    }
+
+    for (Class c : Arrays.asList(EmptyAnnotated.class, InheritedEmptyAnnotated.class)) {
+      r = SSLRandomizer.getSSLRandomizerForClass(c);
+      assertEquals(c.toString(), RandomizeSSL.DEFAULT_ODDS, r.ssl, 0.0D);
+      assertEquals(c.toString(), RandomizeSSL.DEFAULT_ODDS, r.clientAuth, 0.0D);
+    }
+
+    for (Class c : Arrays.asList(SimplyAnnotated.class, InheritedEmptyAnnotatationWithOverride.class)) {
+      r = SSLRandomizer.getSSLRandomizerForClass(c);
+      assertEquals(c.toString(), 0.5D, r.ssl, 0.0D);
+      assertEquals(c.toString(), 0.5D, r.clientAuth, 0.0D);
+    }
+    
+    r = SSLRandomizer.getSSLRandomizerForClass(MinAnnotated.class);
+    assertEquals(0.0D, r.ssl, 0.0D);
+    assertEquals(0.0D, r.clientAuth, 0.0D);
+    conf = r.createSSLTestConfig();
+    assertEquals(false, conf.isSSLMode());
+    assertEquals(false, conf.isClientAuthMode());
+    
+    r = SSLRandomizer.getSSLRandomizerForClass(MaxAnnotated.class);
+    assertEquals(1.0D, r.ssl, 0.0D);
+    assertEquals(1.0D, r.clientAuth, 0.0D);
+    conf = r.createSSLTestConfig();
+    assertEquals(true, conf.isSSLMode());
+    assertEquals(true, conf.isClientAuthMode());
+
+    r = SSLRandomizer.getSSLRandomizerForClass(SSlButNoClientAuthAnnotated.class);
+    assertEquals(0.42D, r.ssl, 0.0D);
+    assertEquals(0.42D, r.clientAuth, 0.0D);
+
+    r = SSLRandomizer.getSSLRandomizerForClass(ClientAuthButNoSSLAnnotated.class);
+    assertEquals(RandomizeSSL.DEFAULT_ODDS, r.ssl, 0.0D);
+    assertEquals(0.42D, r.clientAuth, 0.0D);
+
+    for (Class c : Arrays.asList(SSLOutOfRangeAnnotated.class,
+                                 ClientAuthOutOfRangeAnnotated.class,
+                                 InheritedOutOfRangeAnnotated.class)) {
+      expectThrows(IllegalArgumentException.class, () -> {
+          Object trash = SSLRandomizer.getSSLRandomizerForClass(c);
+        });
+    }
+    
+  }
+  public void testSSLRandomizerEffectiveOdds() {
+    assertEquals(RandomizeSSL.DEFAULT_ODDS,
+                 SSLRandomizer.getEffectiveOdds(RandomizeSSL.DEFAULT_ODDS, false, 1), 0.0005D);
+    assertEquals(0.2727D,
+                 SSLRandomizer.getEffectiveOdds(RandomizeSSL.DEFAULT_ODDS, true, 1), 0.0005D);
+    
+    assertEquals(0.0100D, SSLRandomizer.getEffectiveOdds(0.01D, false, 1), 0.0005D);
+    assertEquals(0.1000D, SSLRandomizer.getEffectiveOdds(0.01D, true, 1), 0.0005D);
+    assertEquals(0.6206D, SSLRandomizer.getEffectiveOdds(0.01D, false, 5), 0.0005D);
+    
+    assertEquals(0.5000D, SSLRandomizer.getEffectiveOdds(0.5D, false, 1), 0.0005D);
+    assertEquals(0.5454D, SSLRandomizer.getEffectiveOdds(0.5D, true, 1), 0.0005D);
+    assertEquals(0.8083D, SSLRandomizer.getEffectiveOdds(0.5D, false, 5), 0.0005D);
+    
+    assertEquals(0.8000D, SSLRandomizer.getEffectiveOdds(0.8D, false, 1), 0.0005D);
+    assertEquals(0.8181D, SSLRandomizer.getEffectiveOdds(0.8D, true, 1), 0.0005D);
+    assertEquals(0.9233D, SSLRandomizer.getEffectiveOdds(0.8D, false, 5), 0.0005D);
+
+    // never ever
+    assertEquals(0.0D, SSLRandomizer.getEffectiveOdds(0.0D, false, 1), 0.0D);
+    assertEquals(0.0D, SSLRandomizer.getEffectiveOdds(0.0D, true, 100), 0.0D);
+    assertEquals(0.0D, SSLRandomizer.getEffectiveOdds(0.0D, false, 100), 0.0D);
+    assertEquals(0.0D, SSLRandomizer.getEffectiveOdds(0.0D, true, 10000), 0.0D);
+    assertEquals(0.0D, SSLRandomizer.getEffectiveOdds(0.0D, false, 10000), 0.0D);
+    assertEquals(0.0D, SSLRandomizer.getEffectiveOdds(0.0D, random().nextBoolean(), random().nextInt()), 0.0D);
+    
+    // always
+    assertEquals(1.0D, SSLRandomizer.getEffectiveOdds(1.0D, false, 1), 0.0D);
+    assertEquals(1.0D, SSLRandomizer.getEffectiveOdds(1.0D, true, 100), 0.0D);
+    assertEquals(1.0D, SSLRandomizer.getEffectiveOdds(1.0D, false, 100), 0.0D);
+    assertEquals(1.0D, SSLRandomizer.getEffectiveOdds(1.0D, true, 10000), 0.0D);
+    assertEquals(1.0D, SSLRandomizer.getEffectiveOdds(1.0D, false, 10000), 0.0D);
+    assertEquals(1.0D, SSLRandomizer.getEffectiveOdds(1.0D, random().nextBoolean(), random().nextInt()), 0.0D);
+    
+  }
+
+  
 }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/09372acb/solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java
----------------------------------------------------------------------
diff --git a/solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java b/solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java
index 072dc90..41554c6 100644
--- a/solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java
+++ b/solr/test-framework/src/java/org/apache/solr/SolrTestCaseJ4.java
@@ -104,6 +104,8 @@ import org.apache.solr.schema.SchemaField;
 import org.apache.solr.search.SolrIndexSearcher;
 import org.apache.solr.servlet.DirectSolrConnection;
 import org.apache.solr.util.AbstractSolrTestCase;
+import org.apache.solr.util.RandomizeSSL;
+import org.apache.solr.util.RandomizeSSL.SSLRandomizer;
 import org.apache.solr.util.RefCounted;
 import org.apache.solr.util.RevertDefaultThreadHandlerRule;
 import org.apache.solr.util.SSLTestConfig;
@@ -137,6 +139,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
 })
 @SuppressSysoutChecks(bugUrl = "Solr dumps tons of logs to console.")
 @SuppressFileSystems("ExtrasFS") // might be ok, the failures with e.g. nightly runs might be "normal"
+@RandomizeSSL()
 public abstract class SolrTestCaseJ4 extends LuceneTestCase {
 
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@@ -317,27 +320,21 @@ public abstract class SolrTestCaseJ4 extends LuceneTestCase {
   }
 
   private static SSLTestConfig buildSSLConfig() {
-    // test has been disabled
-    if (RandomizedContext.current().getTargetClass().isAnnotationPresent(SuppressSSL.class)) {
-      return new SSLTestConfig();
-    }
+
+    SSLRandomizer sslRandomizer =
+      SSLRandomizer.getSSLRandomizerForClass(RandomizedContext.current().getTargetClass());
     
-    // we don't choose ssl that often because of SOLR-5776
-    final boolean trySsl = random().nextInt(10) < 2;
-    // NOTE: clientAuth is useless unless trySsl==true, but we randomize it independently
-    // just in case it might find bugs in our test/ssl client code (ie: attempting to use
-    // SSL w/client cert to non-ssl servers)
-    boolean trySslClientAuth = random().nextInt(10) < 2;
     if (Constants.MAC_OS_X) {
       // see SOLR-9039
       // If a solution is found to remove this, please make sure to also update
       // TestMiniSolrCloudClusterSSL.testSslAndClientAuth as well.
-      trySslClientAuth = false; 
+      sslRandomizer = new SSLRandomizer(sslRandomizer.ssl, 0.0D, (sslRandomizer.debug + " w/ MAC_OS_X supressed clientAuth"));
     }
-    
-    log.info("Randomized ssl ({}) and clientAuth ({})", trySsl, trySslClientAuth);
-    
-    return new SSLTestConfig(trySsl, trySslClientAuth);
+
+    SSLTestConfig result = sslRandomizer.createSSLTestConfig();
+    log.info("Randomized ssl ({}) and clientAuth ({}) via: {}",
+             result.isSSLMode(), result.isClientAuthMode(), sslRandomizer.debug);
+    return result;
   }
 
   protected static JettyConfig buildJettyConfig(String context) {

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/09372acb/solr/test-framework/src/java/org/apache/solr/util/RandomizeSSL.java
----------------------------------------------------------------------
diff --git a/solr/test-framework/src/java/org/apache/solr/util/RandomizeSSL.java b/solr/test-framework/src/java/org/apache/solr/util/RandomizeSSL.java
new file mode 100644
index 0000000..e7336d8
--- /dev/null
+++ b/solr/test-framework/src/java/org/apache/solr/util/RandomizeSSL.java
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.util;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.apache.lucene.util.LuceneTestCase;
+import org.apache.lucene.util.TestUtil;
+import org.apache.solr.SolrTestCaseJ4.SuppressSSL;
+
+
+/**
+ * Marker annotation indicating when SSL Randomization should be used for a test class, and if so what 
+ * the typical odds of using SSL should for that test class.
+ * @see SSLRandomizer#getSSLRandomizerForClass
+ * @see SuppressSSL
+ */
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface RandomizeSSL {
+
+  // we don't choose ssl that often by default because of SOLR-5776
+  public static final double DEFAULT_ODDS = 0.2D;
+  /** Comment to inlcude when logging details of SSL randomization */
+  public String reason() default "";
+  /** 
+   * Odds (as ratio relative to 1) that SSL should be selected in a typical run.
+   * Must either be betwen 0.0 and 1.0 (inclusively) or NaN in which case a sensible should be used.
+   * Actual Odds used for randomization may be higher depending on runner options such as 
+   * <code>tests.multiplier</code> or <code>tests.nightly</code>
+   *
+   * @see #DEFAULT_ODDS
+   * @see LuceneTestCase#TEST_NIGHTLY
+   * @see LuceneTestCase#RANDOM_MULTIPLIER
+   */
+  public double ssl() default Double.NaN;
+  /** 
+   * Odds (as ratio relative to 1) that SSL should be selected in a typical run.
+   * Must either be betwen 0.0 and 1.0 (inclusively) or NaN in which case the effective value of
+   * {@link #ssl} should be used.
+   * Actual Odds used for randomization may be higher depending on runner options such as 
+   * <code>tests.multiplier</code> or <code>tests.nightly</code>
+   * <p>
+   * NOTE: clientAuth is useless unless ssl is also in used, but we randomize it independently
+   * just in case it might find bugs in our test/ssl client code (ie: attempting to use
+   * SSL w/client cert to non-ssl servers)
+   * </p>
+   * @see #DEFAULT_ODDS
+   * @see LuceneTestCase#TEST_NIGHTLY
+   * @see LuceneTestCase#RANDOM_MULTIPLIER
+   */
+  public double clientAuth() default Double.NaN;
+  /**
+   * A shorthand option for controlling both {@link #ssl} and {@link #clientAuth} with a single numeric 
+   * value, For example: <code>@RandomizeSSL(0.5)</code>.
+   *
+   * Ignored if {@link #ssl} is set explicitly.
+   */
+  public double value() default Double.NaN;
+
+  /**
+   * A simple data structure for encapsulating the effective values to be used when randomizing
+   * SSL in a test, based on the configured values in the {@link RandomizeSSL} annotation.
+   */
+  public static final class SSLRandomizer {
+    public final double ssl;
+    public final double clientAuth;
+    public final String debug;
+    /** @lucene.internal */
+    public SSLRandomizer(double ssl, double clientAuth, String debug) {
+      this.ssl = ssl;
+      this.clientAuth = clientAuth;
+      this.debug = debug;
+    }
+
+    /** 
+     * Randomly produces an SSLTestConfig taking into account various factors 
+     *
+     * @see LuceneTestCase#TEST_NIGHTLY
+     * @see LuceneTestCase#RANDOM_MULTIPLIER
+     * @see LuceneTestCase#random()
+     */
+    public SSLTestConfig createSSLTestConfig() {
+      // even if we know SSL is disabled, always consume the same amount of randomness
+      // that way all other test behavior should be consistent even if a user adds/removes @SuppressSSL
+      
+      final boolean useSSL = TestUtil.nextInt(LuceneTestCase.random(), 0, 1000) < 
+        (int)(1000 * getEffectiveOdds(ssl, LuceneTestCase.TEST_NIGHTLY, LuceneTestCase.RANDOM_MULTIPLIER));
+      final boolean useClientAuth = TestUtil.nextInt(LuceneTestCase.random(), 0, 1000) < 
+        (int)(1000 * getEffectiveOdds(clientAuth, LuceneTestCase.TEST_NIGHTLY, LuceneTestCase.RANDOM_MULTIPLIER));
+
+      return new SSLTestConfig(useSSL, useClientAuth);
+    }
+    
+    /** @lucene.internal Public only for testing */
+    public static double getEffectiveOdds(final double declaredOdds,
+                                          final boolean nightly,
+                                          final int multiplier) {
+      assert declaredOdds <= 1.0D;
+      assert 0.0D <= declaredOdds;
+
+      if (declaredOdds == 0.0D || declaredOdds == 1.0D ) {
+        return declaredOdds;
+      }
+      
+      assert 0 < multiplier;
+      
+      // negate the odds so we can then divide it by our multipling factors
+      // to increase the final odds
+      return 1.0D - ((1.0D - declaredOdds)
+                     / ((nightly ? 1.1D : 1.0D) * (1.0D + Math.log(multiplier))));
+    }
+    
+    /**
+     * Returns an SSLRandomizer suitable for the specified (test) class
+     */
+    public static final SSLRandomizer getSSLRandomizerForClass(Class clazz) {
+
+      final SuppressSSL suppression = (SuppressSSL) clazz.getAnnotation(SuppressSSL.class);
+      if (null != suppression) {
+        // Even if this class has a RandomizeSSL annotation, any usage of SuppressSSL -- even in a
+        // super class -- overrules that.
+        //
+        // (If it didn't work this way, it would be a pain in the ass to quickly disable SSL for a
+        // broad hierarchy of tests)
+        return new SSLRandomizer(0.0D, 0.0D, suppression.toString());
+      }
+
+      final RandomizeSSL annotation = (RandomizeSSL) clazz.getAnnotation(RandomizeSSL.class);
+      
+      if (null == annotation) {
+        return new SSLRandomizer(0.0D, 0.0D, RandomizeSSL.class.getName() + " annotation not specified");
+      }
+
+      final double def = Double.isNaN(annotation.value()) ? DEFAULT_ODDS : annotation.value();
+      if (def < 0.0D || 1.0D < def) {
+        throw new IllegalArgumentException
+          (clazz.getName() + ": default value is not a ratio between 0 and 1: " + annotation.toString());
+      }
+      final double ssl = Double.isNaN(annotation.ssl()) ? def : annotation.ssl();
+      if (ssl < 0.0D || 1.0D < ssl) {
+        throw new IllegalArgumentException
+          (clazz.getName() + ": ssl value is not a ratio between 0 and 1: " + annotation.toString());
+      }
+      final double clientAuth = Double.isNaN(annotation.clientAuth()) ? ssl : annotation.clientAuth();
+      if (clientAuth < 0.0D || 1 < clientAuth) {
+        throw new IllegalArgumentException
+          (clazz.getName() + ": clientAuth value is not a ratio between 0 and 1: " + annotation.toString());
+      }
+      return new SSLRandomizer(ssl, clientAuth, annotation.toString());
+    }
+  }
+}