content: /**
   * 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 org.apache.aurora.scheduler.config;
  
  import java.io.File;
  import java.io.IOException;
  import java.lang.reflect.Field;
  import java.net.InetSocketAddress;
  import java.util.List;
  import java.util.Map;
  
  import javax.security.auth.kerberos.KerberosPrincipal;
  
  import com.beust.jcommander.Parameter;
  import com.google.common.base.Joiner;
  import com.google.common.base.Predicates;
  import com.google.common.collect.FluentIterable;
  import com.google.common.collect.ImmutableList;
  import com.google.common.collect.ImmutableMap;
  import com.google.common.collect.Iterables;
  import com.google.common.collect.Lists;
  import com.google.inject.AbstractModule;
  import com.google.inject.Guice;
  import com.google.inject.Injector;
  
  import org.apache.aurora.common.quantity.Data;
  import org.apache.aurora.common.quantity.Time;
  import org.apache.aurora.gen.Container;
  import org.apache.aurora.gen.DockerParameter;
  import org.apache.aurora.gen.Mode;
  import org.apache.aurora.gen.Volume;
  import org.apache.aurora.scheduler.app.MoreModules;
  import org.apache.aurora.scheduler.app.SchedulerMain.Options.DriverKind;
  import org.apache.aurora.scheduler.config.types.DataAmount;
  import org.apache.aurora.scheduler.config.types.TimeAmount;
  import org.apache.aurora.scheduler.http.api.security.HttpSecurityModule.Options.HttpAuthenticationMechanism;
  import org.apache.aurora.scheduler.http.api.security.ShiroIniConverterTest;
  import org.apache.aurora.scheduler.offers.OfferOrder;
  import org.apache.aurora.scheduler.sla.MetricCalculator.MetricCategory;
  import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
  import org.apache.shiro.config.Ini;
  import org.apache.shiro.config.Ini.Section;
  import org.apache.shiro.web.filter.authc.AnonymousFilter;
  import org.junit.Before;
  import org.junit.Test;
  
  import static org.apache.aurora.scheduler.http.api.security.ShiroIniConverterTest.EXAMPLE_RESOURCE;
  import static org.junit.Assert.assertEquals;
  import static org.junit.Assert.fail;
  
  public class CommandLineTest {
    @Before
    public void setUp() {
      CommandLine.initializeForTest();
    }
  
    @Test
    public void testCustomOptions() {
      CommandLine.clearForTest();
  
      // Ensures that a custom module can register command line arguments.
  
      CliOptions options = CommandLine.parseOptions(
          "-task_assigner_modules=org.apache.aurora.scheduler.config.CustomModule",
          "-cluster_name=test",
          "-mesos_master_address=localhost:8080",
          "-backup_dir=/dev/null",
          "-serverset_path=/tmp",
          "-zk_endpoints=localhost:2181",
          "-custom_flag=customValue");
  
      Injector injector = Guice.createInjector(
          MoreModules.instantiateAll(options.state.taskAssignerModules, options));
  
      assertEquals("customValue", injector.getInstance(CustomModule.BINDING_KEY));
    }
  
    public static class NoopModule extends AbstractModule {
      @Override
      protected void configure() {
        // No-op.
      }
    }
  
    private static final TimeAmount TEST_TIME = new TimeAmount(42, Time.DAYS);
    private static final DataAmount TEST_DATA = new DataAmount(42, Data.GB);
  
    @Test
    public void testParseAllOptions() {
      File tempFile;
      try {
        tempFile = File.createTempFile(getClass().getCanonicalName(), null);
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
      tempFile.deleteOnExit();
  
      CliOptions expected = new CliOptions();
      expected.reconciliation.transientTaskStateTimeout = TEST_TIME;
      expected.reconciliation.initialTaskKillRetryInterval = TEST_TIME;
      expected.reconciliation.reconciliationInitialDelay = TEST_TIME;
      expected.reconciliation.reconciliationExplicitInterval = TEST_TIME;
      expected.reconciliation.reconciliationImplicitInterval = TEST_TIME;
      expected.reconciliation.reconciliationScheduleSpread = TEST_TIME;
      expected.reconciliation.reconciliationBatchSize = 42;
      expected.reconciliation.reconciliationBatchInterval = TEST_TIME;
      expected.offer.holdOffersForever = true;
      expected.offer.minOfferHoldTime = TEST_TIME;
      expected.offer.offerHoldJitterWindow = TEST_TIME;
      expected.offer.offerStaticBanCacheMaxSize = 42L;
      expected.offer.offerFilterDuration = TEST_TIME;
      expected.offer.unavailabilityThreshold = TEST_TIME;
      expected.offer.offerOrder = ImmutableList.of(OfferOrder.CPU, OfferOrder.DISK);
      expected.offer.offerSetModule = NoopModule.class;
      expected.executor.customExecutorConfig = tempFile;
      expected.executor.thermosExecutorPath = "testing";
      expected.executor.thermosExecutorResources = ImmutableList.of("testing");
      expected.executor.thermosExecutorFlags = "testing";
      expected.executor.thermosHomeInSandbox = true;
      expected.executor.executorOverheadCpus = 42;
      expected.executor.executorOverheadRam = new DataAmount(42, Data.GB);
      expected.executor.globalContainerMounts =
          ImmutableList.of(new Volume("/container", "/host", Mode.RO));
      expected.executor.populateDiscoveryInfo = true;
      expected.app.maxTasksPerJob = 42;
      expected.app.maxUpdateInstanceFailures = 42;
      expected.app.allowedContainerTypes = ImmutableList.of(Container._Fields.DOCKER);
      expected.app.enableDockerParameters = true;
      expected.app.defaultDockerParameters = ImmutableList.of(new DockerParameter("a", "testing"));
      expected.app.requireDockerUseExecutor = false;
      expected.app.enableMesosFetcher = true;
      expected.app.allowContainerVolumes = true;
      expected.app.allowedJobEnvironments = "^(foo|bar|zaa)$";
      expected.main.clusterName = "testing";
      expected.main.serversetPath = "testing";
      expected.main.serversetEndpointName = "testing";
      expected.main.statsUrlPrefix = "testing";
      expected.main.allowGpuResource = true;
      expected.main.driverImpl = DriverKind.V0_DRIVER;
      expected.scheduling.maxScheduleAttemptsPerSec = 42;
      expected.scheduling.flappingThreshold = TEST_TIME;
      expected.scheduling.initialFlappingDelay = TEST_TIME;
      expected.scheduling.maxFlappingDelay = TEST_TIME;
      expected.scheduling.maxReschedulingDelay = new TimeAmount(42, Time.DAYS);
      expected.scheduling.firstScheduleDelay = TEST_TIME;
      expected.scheduling.initialSchedulePenalty = TEST_TIME;
      expected.scheduling.maxSchedulePenalty = TEST_TIME;
      expected.scheduling.reservationDuration = TEST_TIME;
      expected.scheduling.schedulingMaxBatchSize = 42;
      expected.scheduling.maxTasksPerScheduleAttempt = 42;
      expected.async.asyncWorkerThreads = 42;
      expected.zk.inProcess = true;
      expected.zk.zkEndpoints = ImmutableList.of(InetSocketAddress.createUnresolved("testing", 42));
      expected.zk.chrootPath = "testing";
      expected.zk.sessionTimeout = TEST_TIME;
      expected.zk.connectionTimeout = TEST_TIME;
      expected.zk.digestCredentials = "testing";
      expected.updater.enableAffinity = true;
      expected.updater.affinityExpiration = TEST_TIME;
      expected.updater.slaAwareActionMaxBatchSize = 42;
      expected.updater.slaAwareKillRetryMinDelay = new TimeAmount(42, Time.DAYS);
      expected.updater.slaAwareKillRetryMaxDelay = new TimeAmount(42, Time.DAYS);
      expected.state.taskAssignerModules = ImmutableList.of(NoopModule.class);
      expected.snapshot.snapshotInterval = TEST_TIME;
      expected.logPersistence.maxLogEntrySize = TEST_DATA;
      expected.backup.backupInterval = TEST_TIME;
      expected.backup.maxSavedBackups = 42;
      expected.backup.backupDir = new File("testing");
      expected.aop.methodInterceptorModules = ImmutableList.of(NoopModule.class);
      expected.pruning.historyPruneThreshold = TEST_TIME;
      expected.pruning.historyMaxPerJobThreshold = 42;
      expected.pruning.historyMinRetentionThreshold = TEST_TIME;
      expected.pruning.jobUpdateHistoryPerJobThreshold = 42;
      expected.pruning.jobUpdateHistoryPruningInterval = TEST_TIME;
      expected.pruning.jobUpdateHistoryPruningThreshold = TEST_TIME;
      expected.driver.mesosMasterAddress = "testing";
      expected.driver.frameworkAuthenticationFile = new File("testing");
      expected.driver.frameworkFailoverTimeout = TEST_TIME;
      expected.driver.frameworkAnnouncePrincipal = true;
      expected.driver.frameworkName = "testing";
      expected.driver.executorUser = "testing";
      expected.driver.receiveRevocableResources = true;
      expected.driver.mesosRole = "testing";
      expected.driver.isPartitionAware = true;
      expected.jetty.hostnameOverride = "testing";
      expected.jetty.httpPort = 42;
      expected.jetty.listenIp = "testing";
      expected.httpSecurity.shiroRealmModule = ImmutableList.of(NoopModule.class);
      expected.httpSecurity.shiroAfterAuthFilter = AnonymousFilter.class;
      expected.httpSecurity.httpAuthenticationMechanism = HttpAuthenticationMechanism.NEGOTIATE;
      expected.kerberos.serverKeytab = new File("testing");
      expected.kerberos.serverPrincipal =
          new KerberosPrincipal("HTTP/aurora.example.com@EXAMPLE.COM");
      expected.kerberos.kerberosDebug = true;
  
      Ini testIni = new Ini();
      Section users = testIni.addSection("users");
      users.putAll(ImmutableMap.of(
          "root", "secret, admin",
          "wfarner", "password, eng",
          "ksweeney", "12345"));
      users.put("root", "secret, admin");
      Section roles = testIni.addSection("roles");
      roles.putAll(ImmutableMap.of(
          "admin", "*",
          "eng", "thrift.AuroraSchedulerManager:*"));
  
      expected.iniShiroRealm.shiroIniPath = testIni;
      expected.iniShiroRealm.shiroCredentialsMatcher = AllowAllCredentialsMatcher.class;
      expected.api.enableCorsFor = "testing";
      expected.preemptor.enablePreemptor = false;
      expected.preemptor.preemptionDelay = TEST_TIME;
      expected.preemptor.preemptionSlotHoldTime = TEST_TIME;
      expected.preemptor.preemptionSlotSearchInitialDelay = TEST_TIME;
      expected.preemptor.preemptionSlotSearchInterval = TEST_TIME;
      expected.preemptor.reservationMaxBatchSize = 42;
      expected.preemptor.slotFinderModules = ImmutableList.of(NoopModule.class);
      expected.mesosLog.quorumSize = 42;
      expected.mesosLog.logPath = new File("testing");
      expected.mesosLog.zkLogGroupPath = "testing";
      expected.mesosLog.coordinatorElectionTimeout = TEST_TIME;
      expected.mesosLog.coordinatorElectionRetries = 42;
      expected.mesosLog.readTimeout = TEST_TIME;
      expected.mesosLog.writeTimeout = TEST_TIME;
      expected.sla.minRequiredInstances = 42;
      expected.sla.maxParallelCoordinators = 42;
      expected.sla.maxSlaDuration = TEST_TIME;
      expected.sla.slaCoordinatorTimeout = TEST_TIME;
      expected.sla.slaProdMetrics = ImmutableList.of(MetricCategory.JOB_UPTIMES);
      expected.sla.slaNonProdMetrics = ImmutableList.of(MetricCategory.JOB_UPTIMES);
      expected.sla.slaRefreshInterval = TEST_TIME;
      expected.sla.slaAwareKillNonProd = true;
      expected.webhook.webhookConfigFile = tempFile;
      expected.scheduler.maxRegistrationDelay = TEST_TIME;
      expected.scheduler.maxLeadingDuration = TEST_TIME;
      expected.scheduler.maxStatusUpdateBatchSize = 42;
      expected.scheduler.maxTaskEventBatchSize = 42;
      expected.tiers.tierConfigFile = tempFile;
      expected.asyncStats.taskStatInterval = TEST_TIME;
      expected.asyncStats.slotStatInterval = TEST_TIME;
      expected.stats.samplingInterval = TEST_TIME;
      expected.stats.retentionPeriod = TEST_TIME;
      expected.cron.cronSchedulerNumThreads = 42;
      expected.cron.cronTimezone = "testing";
      expected.cron.cronStartInitialBackoff = TEST_TIME;
      expected.cron.cronStartMaxBackoff = TEST_TIME;
      expected.cron.cronMaxBatchSize = 42;
      expected.resourceSettings.enableRevocableCpus = false;
      expected.resourceSettings.enableRevocableRam = true;
      expected.maintenance.hostMaintenancePollingInterval = TEST_TIME;
  
      assertAllNonDefaultParameters(expected);
  
      CliOptions parsed = CommandLine.parseOptions(
          "-transient_task_state_timeout=42days",
          "-initial_task_kill_retry_interval=42days",
          "-reconciliation_initial_delay=42days",
          "-reconciliation_explicit_interval=42days",
          "-reconciliation_implicit_interval=42days",
          "-reconciliation_schedule_spread=42days",
          "-reconciliation_explicit_batch_size=42",
          "-reconciliation_explicit_batch_interval=42days",
          "-hold_offers_forever=true",
          "-min_offer_hold_time=42days",
          "-offer_hold_jitter_window=42days",
          "-offer_filter_duration=42days",
          "-unavailability_threshold=42days",
          "-offer_order=CPU,DISK",
          "-offer_set_module=org.apache.aurora.scheduler.config.CommandLineTest$NoopModule",
          "-offer_static_ban_cache_max_size=42",
          "-custom_executor_config=" + tempFile.getAbsolutePath(),
          "-thermos_executor_path=testing",
          "-thermos_executor_resources=testing",
          "-thermos_executor_flags=testing",
          "-thermos_home_in_sandbox=true",
          "-thermos_executor_cpu=42",
          "-thermos_executor_ram=42GB",
          "-global_container_mounts=/host:/container:ro",
          "-populate_discovery_info=true",
          "-max_tasks_per_job=42",
          "-max_update_instance_failures=42",
          "-allowed_container_types=DOCKER",
          "-allow_docker_parameters=true",
          "-default_docker_parameters=a=testing",
          "-require_docker_use_executor=false",
          "-enable_mesos_fetcher=true",
          "-allow_container_volumes=true",
          "-allowed_job_environments=^(foo|bar|zaa)$",
          "-cluster_name=testing",
          "-serverset_path=testing",
          "-serverset_endpoint_name=testing",
          "-viz_job_url_prefix=testing",
          "-allow_gpu_resource=true",
          "-mesos_driver=V0_DRIVER",
          "-max_schedule_attempts_per_sec=42",
          "-flapping_task_threshold=42days",
          "-initial_flapping_task_delay=42days",
          "-max_flapping_task_delay=42days",
          "-max_reschedule_task_delay_on_startup=42days",
          "-first_schedule_delay=42days",
          "-initial_schedule_penalty=42days",
          "-max_schedule_penalty=42days",
          "-offer_reservation_duration=42days",
          "-scheduling_max_batch_size=42",
          "-max_tasks_per_schedule_attempt=42",
          "-async_worker_threads=42",
          "-zk_in_proc=true",
          "-zk_endpoints=testing:42",
          "-zk_chroot_path=testing",
          "-zk_session_timeout=42days",
          "-zk_connection_timeout=42days",
          "-zk_digest_credentials=testing",
          "-enable_update_affinity=true",
          "-update_affinity_reservation_hold_time=42days",
          "-sla_aware_action_max_batch_size=42",
          "-sla_aware_kill_retry_min_delay=42days",
          "-sla_aware_kill_retry_max_delay=42days",
          "-sla_aware_kill_non_prod=true",
          "-task_assigner_modules=org.apache.aurora.scheduler.config.CommandLineTest$NoopModule",
          "-dlog_snapshot_interval=42days",
          "-dlog_max_entry_size=42GB",
          "-backup_interval=42days",
          "-max_saved_backups=42",
          "-backup_dir=testing",
          "-thrift_method_interceptor_modules="
              + "org.apache.aurora.scheduler.config.CommandLineTest$NoopModule",
          "-history_prune_threshold=42days",
          "-history_max_per_job_threshold=42",
          "-history_min_retention_threshold=42days",
          "-job_update_history_per_job_threshold=42",
          "-job_update_history_pruning_interval=42days",
          "-job_update_history_pruning_threshold=42days",
          "-mesos_master_address=testing",
          "-framework_authentication_file=testing",
          "-framework_failover_timeout=42days",
          "-framework_announce_principal=true",
          "-framework_name=testing",
          "-executor_user=testing",
          "-receive_revocable_resources=true",
          "-mesos_role=testing",
          "-hostname=testing",
          "-http_port=42",
          "-ip=testing",
          "-shiro_realm_modules=org.apache.aurora.scheduler.config.CommandLineTest$NoopModule",
          "-shiro_after_auth_filter=org.apache.shiro.web.filter.authc.AnonymousFilter",
          "-http_authentication_mechanism=NEGOTIATE",
          "-kerberos_server_keytab=testing",
          "-kerberos_server_principal=HTTP/aurora.example.com@EXAMPLE.COM",
          "-kerberos_debug=true",
          "-shiro_ini_path=" + ShiroIniConverterTest.class.getResource(EXAMPLE_RESOURCE).toString(),
          "-shiro_credentials_matcher="
              + "org.apache.shiro.authc.credential.AllowAllCredentialsMatcher",
          "-enable_cors_for=testing",
          "-enable_preemptor=false",
          "-preemption_delay=42days",
          "-preemption_slot_hold_time=42days",
          "-preemption_slot_search_initial_delay=42days",
          "-preemption_slot_search_interval=42days",
          "-preemption_reservation_max_batch_size=42",
          "-preemption_slot_finder_modules="
              + "org.apache.aurora.scheduler.config.CommandLineTest$NoopModule",
          "-native_log_quorum_size=42",
          "-native_log_file_path=testing",
          "-native_log_zk_group_path=testing",
          "-native_log_election_timeout=42days",
          "-native_log_election_retries=42",
          "-native_log_read_timeout=42days",
          "-native_log_write_timeout=42days",
          "-sla_stat_refresh_interval=42days",
          "-sla_prod_metrics=JOB_UPTIMES",
          "-sla_non_prod_metrics=JOB_UPTIMES",
          "-webhook_config=" + tempFile.getAbsolutePath(),
          "-max_registration_delay=42days",
          "-max_leading_duration=42days",
          "-max_status_update_batch_size=42",
          "-max_task_event_batch_size=42",
          "-tier_config=" + tempFile.getAbsolutePath(),
          "-async_task_stat_update_interval=42days",
          "-async_slot_stat_update_interval=42days",
          "-stat_sampling_interval=42days",
          "-stat_retention_period=42days",
          "-cron_scheduler_num_threads=42",
          "-cron_timezone=testing",
          "-cron_start_initial_backoff=42days",
          "-cron_start_max_backoff=42days",
          "-cron_scheduling_max_batch_size=42",
          "-enable_revocable_cpus=false",
          "-enable_revocable_ram=true",
          "-partition_aware=true",
          "-sla_coordinator_timeout=42days",
          "-host_maintenance_polling_interval=42days",
          "-max_parallel_coordinated_maintenance=42",
          "-min_required_instances_for_sla_check=42",
          "-max_sla_duration_secs=42days"
      );
      assertEqualOptions(expected, parsed);
    }
  
    @Test
    public void testEmptyListOptions() {
  
      // This test ensures that all list options are capable of returning
      // empty lists.
  
      CliOptions expected = new CliOptions();
      expected.main.clusterName = "testing";
      expected.driver.mesosMasterAddress = "testing";
      expected.backup.backupDir = new File("testing");
      expected.main.serversetPath = "testing";
      expected.zk.zkEndpoints = ImmutableList.of(InetSocketAddress.createUnresolved("testing", 42));
      expected.offer.offerOrder = ImmutableList.of();
      expected.executor.thermosExecutorResources = ImmutableList.of();
      expected.executor.globalContainerMounts = ImmutableList.of();
      expected.app.allowedContainerTypes = ImmutableList.of();
      expected.app.defaultDockerParameters = ImmutableList.of();
      expected.state.taskAssignerModules = ImmutableList.of();
      expected.aop.methodInterceptorModules = ImmutableList.of();
      expected.httpSecurity.shiroRealmModule = ImmutableList.of();
      expected.preemptor.slotFinderModules = ImmutableList.of();
      expected.sla.slaProdMetrics = ImmutableList.of();
      expected.sla.slaNonProdMetrics = ImmutableList.of();
  
      CliOptions parsed = CommandLine.parseOptions(
              "-cluster_name=testing",
              "-mesos_master_address=testing",
              "-backup_dir=testing",
              "-serverset_path=testing",
              "-zk_endpoints=testing:42",
              "-offer_order=",
              "-thermos_executor_resources=",
              "-global_container_mounts=",
              "-allowed_container_types=",
              "-default_docker_parameters=",
              "-task_assigner_modules=",
              "-thrift_method_interceptor_modules=",
              "-shiro_realm_modules=",
              "-preemption_slot_finder_modules=",
              "-sla_prod_metrics=",
              "-sla_non_prod_metrics=");
  
      assertEqualOptions(expected, parsed);
  
      // Test other ways in which an empty list can be passed
      // as thermos executor resources such as "" and ''.
      parsed = CommandLine.parseOptions(
              "-cluster_name=testing",
              "-mesos_master_address=testing",
              "-backup_dir=testing",
              "-serverset_path=testing",
              "-zk_endpoints=testing:42",
              "-thermos_executor_resources=\"\"");
  
      assertEquals(ImmutableList.of(), parsed.executor.thermosExecutorResources);
  
      parsed = CommandLine.parseOptions(
              "-cluster_name=testing",
              "-mesos_master_address=testing",
              "-backup_dir=testing",
              "-serverset_path=testing",
              "-zk_endpoints=testing:42",
              "-thermos_executor_resources=''");
  
      assertEquals(ImmutableList.of(), parsed.executor.thermosExecutorResources);
    }
  
    private static void assertEqualOptions(CliOptions expected, CliOptions actual) {
      List<Object> actualObjects = CommandLine.getOptionsObjects(actual);
      for (Object expectedOptionContainer : CommandLine.getOptionsObjects(expected)) {
        Iterable<Field> paramFields = FluentIterable
            .from(expectedOptionContainer.getClass().getDeclaredFields())
            .filter(f -> f.getAnnotation(Parameter.class) != null);
        Object actualOptionContainer = FluentIterable.from(actualObjects)
            .firstMatch(Predicates.instanceOf(expectedOptionContainer.getClass()))
            .get();
        for (Field field : paramFields) {
          Parameter declaration = field.getAnnotation(Parameter.class);
          try {
            assertEquals(String.format("Value for %s does not match", declaration.names()[0]),
                field.get(expectedOptionContainer),
                field.get(actualOptionContainer));
          } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
          }
        }
      }
    }
  
    private static void assertAllNonDefaultParameters(CliOptions testCase) {
      // This is a safety check for the test setup itself.  We want to ensure that all options are
      // set up to parse correctly (e.g. have types that the parsing system is set up to handle).
      // To do this, check that the expected parse result sets all parameter fields to non-default
      // values.
      List<Object> defaultContainers = CommandLine.getOptionsObjects(new CliOptions());
  
      List<String> errors = Lists.newArrayList();
  
      for (Object testCaseContainer : CommandLine.getOptionsObjects(testCase)) {
        Iterable<Field> paramFields = FluentIterable
            .from(testCaseContainer.getClass().getDeclaredFields())
            .filter(f -> f.getAnnotation(Parameter.class) != null);
        Object defaultObject = FluentIterable.from(defaultContainers)
            .firstMatch(Predicates.instanceOf(testCaseContainer.getClass()))
            .get();
        for (Field field : paramFields) {
          Parameter declaration = field.getAnnotation(Parameter.class);
          try {
            String desc =
                String.format("for field %s (option %s)", field.getName(), declaration.names()[0]);
            Object testCaseOptionValue = field.get(testCaseContainer);
            if (testCaseOptionValue == null) {
              errors.add(String.format("Test case value may not be null %s", desc));
              continue;
            }
            if (testCaseOptionValue instanceof Iterable) {
              Iterable<?> value = (Iterable<?>) testCaseOptionValue;
              if (Iterables.isEmpty(value)) {
                errors.add(String.format("Test case value may not be empty %s", desc));
                continue;
              }
            }
            if (testCaseOptionValue instanceof Map) {
              Map<?, ?> value = (Map<?, ?>) testCaseOptionValue;
              if (value.isEmpty()) {
                errors.add(String.format("Test case value may not be empty %s", desc));
                continue;
              }
            }
            if (testCaseOptionValue.equals(field.get(defaultObject))) {
              errors.add(String.format("Test case may not use default option value %s", desc));
            }
          } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
          }
        }
      }
  
      if (!errors.isEmpty()) {
        fail("Test case is incomplete:\n  " + Joiner.on("\n  ").join(errors));
      }
    }
  }
  