http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/thrift/com/twitter/thrift/endpoint.thrift ---------------------------------------------------------------------- diff --git a/commons/src/main/thrift/com/twitter/thrift/endpoint.thrift b/commons/src/main/thrift/com/twitter/thrift/endpoint.thrift new file mode 100644 index 0000000..2d14541 --- /dev/null +++ b/commons/src/main/thrift/com/twitter/thrift/endpoint.thrift @@ -0,0 +1,115 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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. +// ================================================================================================= + +// Author: jsirois + +// TODO(wickman) Fix uses of this in python+science! Especially expertsearch +// This should be aliased against twitter.thrift or twitter.common.service, or +// wherever the python service discovery stack lands. + +namespace java com.twitter.thrift +#@namespace scala com.twitter.thrift.endpoint.thriftscala +namespace rb Twitter.Thrift +namespace py gen.twitter.thrift.endpoint + +/* + * Represents the status of a service. + */ +enum Status { + + /* + * The service is dead and can no longer be contacted. + */ + DEAD = 0, + + /* + * The service is in the process of starting up for the first time or from a STOPPED state. + */ + STARTING = 1, + + /* + * The service is alive and ready to receive requests. + */ + ALIVE = 2, + + /* + * The service is in the process of stopping and should no longer be contacted. In this state + * well behaved services will typically finish existing requests but accept no new rtequests. + */ + STOPPING = 3, + + /* + * The service is stopped and cannot be contacted unless started again. + */ + STOPPED = 4, + + /* + * The service is alive but in a potentially bad state. + */ + WARNING = 5, +} + +/* + * Represents a TCP service network endpoint. + */ +struct Endpoint { + + /* + * The remote hostname or ip address of the endpoint. + */ + 1: string host + + /* + * The TCP port the endpoint listens on. + */ + 2: i32 port +} + +/* + * Represents information about the state of a service instance. + */ +struct ServiceInstance { + + /* + * Represents the primary service interface endpoint. This is typically a thrift service + * endpoint. + */ + 1: Endpoint serviceEndpoint + + /* + * A mapping of any additional interfaces the service exports. The mapping is from logical + * interface names to endpoints. The map may be empty, but a typical additional endpoint mapping + * would provide the endoint got the "http-admin" debug interface for example. + * + * TODO(John Sirois): consider promoting string -> Enum or adding thrift string constants for common + * service names to help identify common beasts like ostrich-http-admin, ostrich-telnet and + * process-http-admin but still allow for new experimental interfaces as well without having to + * change this thift file. + */ + 2: map<string, Endpoint> additionalEndpoints + + /* + * The status of this service instance. + * NOTE: Only status ALIVE should be used. This field is pending removal. + * TODO(Sathya Hariesh): Remove the status field. + */ + 3: Status status; + + /* + * The shard identifier for this instance. + */ + 4: optional i32 shard; +}
http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/test/java/com/twitter/common/application/AppLauncherTest.java ---------------------------------------------------------------------- diff --git a/commons/src/test/java/com/twitter/common/application/AppLauncherTest.java b/commons/src/test/java/com/twitter/common/application/AppLauncherTest.java new file mode 100644 index 0000000..b428f5e --- /dev/null +++ b/commons/src/test/java/com/twitter/common/application/AppLauncherTest.java @@ -0,0 +1,77 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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.twitter.common.application; + +import java.lang.reflect.Field; + +import com.google.common.base.Predicates; + +import org.junit.Test; + +import com.twitter.common.args.Arg; +import com.twitter.common.args.ArgFilters; +import com.twitter.common.args.CmdLine; +import com.twitter.common.args.constraints.NotNull; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * @author John Sirois + */ +public class AppLauncherTest { + + public static class TestApp1 extends AbstractApplication { + @NotNull + @CmdLine(name = "user", help = "a username") + static final Arg<String> USER = Arg.create(); + + private static boolean hasRun; + + @Override public void run() { + hasRun = true; + } + } + + @Test + public void testLaunch1() { + AppLauncher.launch(TestApp1.class, ArgFilters.selectClass(TestApp1.class), "-user", "jake"); + assertTrue(TestApp1.hasRun); + assertEquals("jake", TestApp1.USER.get()); + } + + public static class TestApp2 extends AbstractApplication { + @NotNull + @CmdLine(name = "user", help = "a username") + static final Arg<String> USER = Arg.create(null); + + private static boolean hasRun; + + @Override public void run() { + hasRun = true; + } + } + + @Test + public void testLaunch2() { + // We filter out the NotNull Arg so we should be able to launch without specifying it. + AppLauncher.launch(TestApp2.class, Predicates.<Field>alwaysFalse()); + assertTrue(TestApp2.hasRun); + assertNull(TestApp2.USER.get()); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/test/java/com/twitter/common/application/modules/LifecycleModuleTest.java ---------------------------------------------------------------------- diff --git a/commons/src/test/java/com/twitter/common/application/modules/LifecycleModuleTest.java b/commons/src/test/java/com/twitter/common/application/modules/LifecycleModuleTest.java new file mode 100644 index 0000000..2d8d61e --- /dev/null +++ b/commons/src/test/java/com/twitter/common/application/modules/LifecycleModuleTest.java @@ -0,0 +1,124 @@ +package com.twitter.common.application.modules; + +import java.lang.Thread.UncaughtExceptionHandler; +import java.net.InetSocketAddress; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableMap; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; + +import org.junit.Test; + +import com.twitter.common.application.ShutdownRegistry.ShutdownRegistryImpl; +import com.twitter.common.application.modules.LifecycleModule.LaunchException; +import com.twitter.common.application.modules.LifecycleModule.ServiceRunner; +import com.twitter.common.application.modules.LocalServiceRegistry.LocalService; +import com.twitter.common.base.Command; +import com.twitter.common.testing.easymock.EasyMockTest; + +import static org.easymock.EasyMock.expect; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import static com.twitter.common.application.modules.LifecycleModule.bindLocalService; +import static com.twitter.common.net.InetSocketAddressHelper.getLocalAddress; + +/** + * @author William Farner + */ +public class LifecycleModuleTest extends EasyMockTest { + + private static class SystemModule extends AbstractModule { + @Override protected void configure() { + install(new LifecycleModule()); + bind(UncaughtExceptionHandler.class).toInstance(new UncaughtExceptionHandler() { + @Override public void uncaughtException(Thread thread, Throwable throwable) { + fail("Uncaught exception."); + } + }); + } + } + + @Test + public void testNoServices() { + control.replay(); + + Injector injector = Guice.createInjector(new SystemModule()); + + LocalServiceRegistry registry = injector.getInstance(LocalServiceRegistry.class); + assertEquals(Optional.<InetSocketAddress>absent(), registry.getPrimarySocket()); + assertEquals(ImmutableMap.<String, InetSocketAddress>of(), registry.getAuxiliarySockets()); + } + + @Test + public void testNoRunner() throws Exception { + final Command primaryShutdown = createMock(Command.class); + final Command auxShutdown = createMock(Command.class); + + primaryShutdown.execute(); + auxShutdown.execute(); + + Module testModule = new AbstractModule() { + @Override protected void configure() { + bindLocalService(binder(), LocalService.primaryService(99, primaryShutdown)); + bindLocalService(binder(), LocalService.auxiliaryService("foo", 100, auxShutdown)); + } + }; + + Injector injector = Guice.createInjector(new SystemModule(), testModule); + LocalServiceRegistry registry = injector.getInstance(LocalServiceRegistry.class); + + control.replay(); + + assertEquals(Optional.of(getLocalAddress(99)), registry.getPrimarySocket()); + assertEquals(ImmutableMap.of("foo", getLocalAddress(100)), registry.getAuxiliarySockets()); + + injector.getInstance(ShutdownRegistryImpl.class).execute(); + } + + @Test + public void testOrdering() throws Exception { + final ServiceRunner runner = createMock(ServiceRunner.class); + Command shutdown = createMock(Command.class); + + expect(runner.launch()).andReturn(LocalService.primaryService(100, shutdown)); + shutdown.execute(); + + Module testModule = new AbstractModule() { + @Override protected void configure() { + LifecycleModule.runnerBinder(binder()).addBinding().toInstance(runner); + } + }; + + Injector injector = Guice.createInjector(new SystemModule(), testModule); + LocalServiceRegistry registry = injector.getInstance(LocalServiceRegistry.class); + + control.replay(); + + assertEquals(Optional.of(getLocalAddress(100)), registry.getPrimarySocket()); + injector.getInstance(ShutdownRegistryImpl.class).execute(); + } + + @Test(expected = IllegalStateException.class) + public void testFailedLauncher() throws Exception { + final ServiceRunner runner = createMock(ServiceRunner.class); + + expect(runner.launch()).andThrow(new LaunchException("Injected failure.")); + + Module testModule = new AbstractModule() { + @Override protected void configure() { + LifecycleModule.runnerBinder(binder()).addBinding().toInstance(runner); + } + }; + + Injector injector = Guice.createInjector(new SystemModule(), testModule); + LocalServiceRegistry registry = injector.getInstance(LocalServiceRegistry.class); + + control.replay(); + + assertEquals(Optional.of(getLocalAddress(100)), registry.getPrimarySocket()); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/test/java/com/twitter/common/application/modules/LocalServiceRegistryTest.java ---------------------------------------------------------------------- diff --git a/commons/src/test/java/com/twitter/common/application/modules/LocalServiceRegistryTest.java b/commons/src/test/java/com/twitter/common/application/modules/LocalServiceRegistryTest.java new file mode 100644 index 0000000..50acdf7 --- /dev/null +++ b/commons/src/test/java/com/twitter/common/application/modules/LocalServiceRegistryTest.java @@ -0,0 +1,157 @@ +package com.twitter.common.application.modules; + +import java.net.InetSocketAddress; +import java.util.Map; +import java.util.Set; + +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import com.google.inject.Provider; + +import org.junit.Before; +import org.junit.Test; + +import com.twitter.common.application.ShutdownRegistry; +import com.twitter.common.application.modules.LifecycleModule.LaunchException; +import com.twitter.common.application.modules.LifecycleModule.ServiceRunner; +import com.twitter.common.application.modules.LocalServiceRegistry.LocalService; +import com.twitter.common.base.Commands; +import com.twitter.common.testing.easymock.EasyMockTest; + +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.expectLastCall; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +/** + * @author William Farner + */ +public class LocalServiceRegistryTest extends EasyMockTest { + + private static final Function<InetSocketAddress, Integer> INET_TO_PORT = + new Function<InetSocketAddress, Integer>() { + @Override public Integer apply(InetSocketAddress address) { + return address.getPort(); + } + }; + + private static final String A = "a"; + private static final String B = "b"; + private static final String C = "c"; + + private ServiceRunner runner1; + private ServiceRunner runner2; + private Provider<Set<ServiceRunner>> serviceProvider; + private ShutdownRegistry shutdownRegistry; + private LocalServiceRegistry registry; + + @Before + public void setUp() { + runner1 = createMock(ServiceRunner.class); + runner2 = createMock(ServiceRunner.class); + serviceProvider = createMock(new Clazz<Provider<Set<ServiceRunner>>>() { }); + shutdownRegistry = createMock(ShutdownRegistry.class); + registry = new LocalServiceRegistry(serviceProvider, shutdownRegistry); + } + + @Test + public void testCreate() throws LaunchException { + expect(serviceProvider.get()).andReturn(ImmutableSet.of(runner1, runner2)); + expect(runner1.launch()).andReturn(primary(1)); + expect(runner2.launch()).andReturn(auxiliary(A, 2)); + shutdownRegistry.addAction(Commands.NOOP); + expectLastCall().times(2); + + control.replay(); + + checkPorts(Optional.of(1), ImmutableMap.of(A, 2)); + } + + private LocalService primary(int port) { + return LocalService.primaryService(port, Commands.NOOP); + } + + private LocalService auxiliary(String name, int port) { + return LocalService.auxiliaryService(name, port, Commands.NOOP); + } + + private LocalService auxiliary(Set<String> names, int port) { + return LocalService.auxiliaryService(names, port, Commands.NOOP); + } + + @Test + public void testNoPrimary() throws LaunchException { + expect(serviceProvider.get()).andReturn(ImmutableSet.of(runner1)); + expect(runner1.launch()).andReturn(auxiliary(A, 2)); + shutdownRegistry.addAction(Commands.NOOP); + expectLastCall().times(1); + + control.replay(); + + assertFalse(registry.getPrimarySocket().isPresent()); + } + + @Test(expected = IllegalArgumentException.class) + public void testMultiplePrimaries() throws LaunchException { + expect(serviceProvider.get()).andReturn(ImmutableSet.of(runner1, runner2)); + expect(runner1.launch()).andReturn(primary(1)); + expect(runner2.launch()).andReturn(primary(2)); + shutdownRegistry.addAction(Commands.NOOP); + expectLastCall().times(2); + + control.replay(); + + registry.getPrimarySocket(); + } + + @Test(expected = IllegalArgumentException.class) + public void testDuplicateName() throws LaunchException { + expect(serviceProvider.get()).andReturn(ImmutableSet.of(runner1, runner2)); + expect(runner1.launch()).andReturn(auxiliary(A, 1)); + expect(runner2.launch()).andReturn(auxiliary(A, 2)); + shutdownRegistry.addAction(Commands.NOOP); + expectLastCall().times(2); + + control.replay(); + + registry.getPrimarySocket(); + } + + @Test + public void testAllowsPortReuse() throws LaunchException { + expect(serviceProvider.get()).andReturn(ImmutableSet.of(runner1, runner2)); + expect(runner1.launch()).andReturn(auxiliary(A, 2)); + expect(runner2.launch()).andReturn(auxiliary(B, 2)); + shutdownRegistry.addAction(Commands.NOOP); + expectLastCall().times(2); + + control.replay(); + + checkPorts(Optional.<Integer>absent(), ImmutableMap.of(A, 2, B, 2)); + } + + @Test + public void testMultiNameBreakout() throws LaunchException { + expect(serviceProvider.get()).andReturn(ImmutableSet.of(runner1, runner2)); + expect(runner1.launch()).andReturn(auxiliary(A, 2)); + expect(runner2.launch()).andReturn(auxiliary(ImmutableSet.of(B, C), 6)); + shutdownRegistry.addAction(Commands.NOOP); + expectLastCall().times(2); + + control.replay(); + + checkPorts(Optional.<Integer>absent(), ImmutableMap.of(A, 2, B, 6, C, 6)); + } + + private void checkPorts(Optional<Integer> primary, Map<String, Integer> expected) { + Optional<InetSocketAddress> registeredSocket = registry.getPrimarySocket(); + Optional<Integer> registeredPort = registeredSocket.isPresent() + ? Optional.of(registeredSocket.get().getPort()) : Optional.<Integer>absent(); + + assertEquals(primary, registeredPort); + assertEquals(expected, Maps.transformValues(registry.getAuxiliarySockets(), INET_TO_PORT)); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/test/java/com/twitter/common/application/modules/StartStatPollerTest.java ---------------------------------------------------------------------- diff --git a/commons/src/test/java/com/twitter/common/application/modules/StartStatPollerTest.java b/commons/src/test/java/com/twitter/common/application/modules/StartStatPollerTest.java new file mode 100644 index 0000000..5def8a8 --- /dev/null +++ b/commons/src/test/java/com/twitter/common/application/modules/StartStatPollerTest.java @@ -0,0 +1,50 @@ +package com.twitter.common.application.modules; + +import java.util.Properties; + +import org.junit.Test; + +import com.twitter.common.application.ShutdownRegistry; +import com.twitter.common.stats.Stat; +import com.twitter.common.stats.Stats; +import com.twitter.common.stats.TimeSeriesRepository; +import com.twitter.common.testing.easymock.EasyMockTest; +import com.twitter.common.util.BuildInfo; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class StartStatPollerTest extends EasyMockTest { + @Test + public void testStartStatPollerExecute() { + ShutdownRegistry shutdownRegistry = createMock(ShutdownRegistry.class); + TimeSeriesRepository repository = createMock(TimeSeriesRepository.class); + + Properties properties = new Properties(); + final Long gitRevisionNumber = 1404461016779713L; + properties.setProperty(BuildInfo.Key.GIT_REVISION_NUMBER.value, gitRevisionNumber.toString()); + String gitRevision = "foo_branch"; + properties.setProperty(BuildInfo.Key.GIT_REVISION.value, gitRevision); + BuildInfo buildInfo = new BuildInfo(properties); + + StatsModule.StartStatPoller poller = + new StatsModule.StartStatPoller(shutdownRegistry, buildInfo, repository); + + repository.start(shutdownRegistry); + control.replay(); + + poller.execute(); + + Stat<Long> gitRevisionNumberStat = + Stats.getVariable(Stats.normalizeName(BuildInfo.Key.GIT_REVISION_NUMBER.value)); + assertEquals(gitRevisionNumber, gitRevisionNumberStat.read()); + + Stat<String> gitRevisionStat = + Stats.getVariable(Stats.normalizeName(BuildInfo.Key.GIT_REVISION.value)); + assertEquals(gitRevision, gitRevisionStat.read()); + + Stat<String> gitBranchNameStat = + Stats.getVariable(Stats.normalizeName(BuildInfo.Key.GIT_BRANCHNAME.value)); + assertNull(gitBranchNameStat); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/test/java/com/twitter/common/args/ArgFiltersTest.java ---------------------------------------------------------------------- diff --git a/commons/src/test/java/com/twitter/common/args/ArgFiltersTest.java b/commons/src/test/java/com/twitter/common/args/ArgFiltersTest.java new file mode 100644 index 0000000..6dc8ad5 --- /dev/null +++ b/commons/src/test/java/com/twitter/common/args/ArgFiltersTest.java @@ -0,0 +1,108 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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.twitter.common.args; + +import java.io.IOException; +import java.lang.reflect.Field; + +import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; + +import org.junit.Test; + +import com.twitter.common.args.apt.Configuration; +import com.twitter.common.args.argfilterstest.ArgsRoot; +import com.twitter.common.args.argfilterstest.subpackageA.ArgsA; +import com.twitter.common.args.argfilterstest.subpackageA.subsubpackage1.ArgsA1; +import com.twitter.common.args.argfilterstest.subpackageB.ArgsB; +import com.twitter.common.args.argfilterstest.subpackageBwithSuffix.ArgsBWithSuffix; + +import static org.junit.Assert.assertEquals; + +import static com.twitter.common.args.apt.Configuration.ArgInfo; + +/** + * @author John Sirois + */ +public class ArgFiltersTest { + + @CmdLine(name = "a", help = "") + static final Arg<String> A = Arg.create(); + + @CmdLine(name = "b", help = "") + static final Arg<String> B = Arg.create(); + + @Test + public void testpackage() throws IOException { + testFilter(ArgFilters.selectPackage(ArgsRoot.class.getPackage()), + fieldInfo(ArgsRoot.class, "ARGS_ROOT")); + } + + @Test + public void testAllPackagesUnderHere() throws IOException { + testFilter(ArgFilters.selectAllPackagesUnderHere(ArgsRoot.class.getPackage()), + fieldInfo(ArgsRoot.class, "ARGS_ROOT"), + fieldInfo(ArgsA.class, "ARGS_A"), + fieldInfo(ArgsB.class, "ARGS_B"), + fieldInfo(ArgsA1.class, "ARGS_A1"), + fieldInfo(ArgsBWithSuffix.class, "ARGS_B_WITH_SUFFIX")); + + testFilter(ArgFilters.selectAllPackagesUnderHere(ArgsB.class.getPackage()), + fieldInfo(ArgsB.class, "ARGS_B")); + } + + @Test + public void testClass() throws IOException { + testFilter(ArgFilters.selectClass(ArgFiltersTest.class), + fieldInfo(ArgFiltersTest.class, "A"), fieldInfo(ArgFiltersTest.class, "B")); + } + + @Test + public void testClasses() throws IOException { + testFilter(ArgFilters.selectClasses(ArgFiltersTest.class, ArgsA.class), + fieldInfo(ArgFiltersTest.class, "A"), + fieldInfo(ArgFiltersTest.class, "B"), + fieldInfo(ArgsA.class, "ARGS_A")); + } + + @Test + public void testArg() throws IOException { + testFilter(ArgFilters.selectCmdLineArg(ArgFiltersTest.class, "b"), + fieldInfo(ArgFiltersTest.class, "B")); + } + + private static ArgInfo fieldInfo(Class<?> declaringClass, String fieldName) { + return new ArgInfo(declaringClass.getName(), fieldName); + } + + private void testFilter(final Predicate<Field> filter, ArgInfo... expected) + throws IOException { + + Predicate<Optional<Field>> fieldFilter = new Predicate<Optional<Field>>() { + @Override public boolean apply(Optional<Field> maybeField) { + return maybeField.isPresent() && filter.apply(maybeField.get()); + } + }; + + assertEquals(ImmutableSet.copyOf(expected), + ImmutableSet.copyOf(Iterables.filter(Configuration.load().optionInfo(), + Predicates.compose(fieldFilter, Args.TO_FIELD)))); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/test/java/com/twitter/common/args/ArgScannerTest.java ---------------------------------------------------------------------- diff --git a/commons/src/test/java/com/twitter/common/args/ArgScannerTest.java b/commons/src/test/java/com/twitter/common/args/ArgScannerTest.java new file mode 100644 index 0000000..3a47f56 --- /dev/null +++ b/commons/src/test/java/com/twitter/common/args/ArgScannerTest.java @@ -0,0 +1,851 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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.twitter.common.args; + +import java.io.File; +import java.io.PrintStream; +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.io.ByteStreams; + +import org.junit.Before; +import org.junit.Test; + +import com.twitter.common.args.ArgScannerTest.StandardArgs.Optimizations; +import com.twitter.common.args.constraints.NotEmpty; +import com.twitter.common.args.constraints.NotNegative; +import com.twitter.common.args.constraints.NotNull; +import com.twitter.common.args.constraints.Positive; +import com.twitter.common.args.constraints.Range; +import com.twitter.common.args.parsers.NonParameterizedTypeParser; +import com.twitter.common.base.Command; +import com.twitter.common.base.Function; +import com.twitter.common.base.MorePreconditions; +import com.twitter.common.collections.Pair; +import com.twitter.common.quantity.Amount; +import com.twitter.common.quantity.Data; +import com.twitter.common.quantity.Time; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author William Farner + */ +public class ArgScannerTest { + + private static final Function<Class<?>, Predicate<Field>> TO_SCOPE_PREDICATE = + new Function<Class<?>, Predicate<Field>>() { + @Override public Predicate<Field> apply(final Class<?> cls) { + return new Predicate<Field>() { + @Override public boolean apply(Field field) { + return field.getDeclaringClass() == cls; + } + }; + } + }; + + @Before + public void setUp() { + // Reset args in all classes before each test. + for (Class<?> cls : this.getClass().getDeclaredClasses()) { + resetArgs(cls); + } + } + + public static class StandardArgs { + enum Optimizations { NONE, MINIMAL, ALL } + @CmdLine(name = "enum", help = "help") + static final Arg<Optimizations> ENUM_VAL = Arg.create(Optimizations.MINIMAL); + @CmdLine(name = "string", help = "help") + static final Arg<String> STRING_VAL = Arg.create("string"); + @CmdLine(name = "char", help = "help") + static final Arg<Character> CHAR_VAL = Arg.create('c'); + @CmdLine(name = "byte", help = "help") + static final Arg<Byte> BYTE_VAL = Arg.create((byte) 0); + @CmdLine(name = "short", help = "help") + static final Arg<Short> SHORT_VAL = Arg.create((short) 0); + @CmdLine(name = "int", help = "help") + static final Arg<Integer> INT_VAL = Arg.create(0); + @CmdLine(name = "long", help = "help") + static final Arg<Long> LONG_VAL = Arg.create(0L); + @CmdLine(name = "float", help = "help") + static final Arg<Float> FLOAT_VAL = Arg.create(0F); + @CmdLine(name = "double", help = "help") + static final Arg<Double> DOUBLE_VAL = Arg.create(0D); + @CmdLine(name = "bool", help = "help") + static final Arg<Boolean> BOOL = Arg.create(false); + @CmdLine(name = "regex", help = "help") + static final Arg<Pattern> REGEX = Arg.create(null); + @CmdLine(name = "time_amount", help = "help") + static final Arg<Amount<Long, Time>> TIME_AMOUNT = Arg.create(Amount.of(1L, Time.SECONDS)); + @CmdLine(name = "data_amount", help = "help") + static final Arg<Amount<Long, Data>> DATA_AMOUNT = Arg.create(Amount.of(1L, Data.MB)); + @CmdLine(name = "range", help = "help") + static final Arg<com.google.common.collect.Range<Integer>> RANGE = + Arg.create(com.google.common.collect.Range.closed(1, 5)); + @Positional(help = "help") + static final Arg<List<Amount<Long, Time>>> POSITIONAL = + Arg.<List<Amount<Long, Time>>>create(ImmutableList.<Amount<Long, Time>>of()); + } + + @Test + public void testStandardArgs() { + test(StandardArgs.class, + new Command() { + @Override public void execute() { + assertThat(StandardArgs.ENUM_VAL.get(), is(Optimizations.ALL)); + } + }, "enum", "ALL"); + test(StandardArgs.class, + new Command() { + @Override public void execute() { + assertThat(StandardArgs.STRING_VAL.get(), is("newstring")); + } + }, + "string", "newstring"); + test(StandardArgs.class, + new Command() { + @Override public void execute() { assertThat(StandardArgs.CHAR_VAL.get(), is('x')); } + }, + "char", "x"); + test(StandardArgs.class, + new Command() { + @Override public void execute() { + assertThat(StandardArgs.BYTE_VAL.get(), is((byte) 10)); + } + }, + "byte", "10"); + test(StandardArgs.class, + new Command() { + @Override public void execute() { + assertThat(StandardArgs.SHORT_VAL.get(), is((short) 10)); + } + }, + "short", "10"); + test(StandardArgs.class, + new Command() { + @Override public void execute() { assertThat(StandardArgs.INT_VAL.get(), is(10)); } + }, + "int", "10"); + test(StandardArgs.class, + new Command() { + @Override public void execute() { assertThat(StandardArgs.LONG_VAL.get(), is(10L)); } + }, + "long", "10"); + test(StandardArgs.class, + new Command() { + @Override public void execute() { assertThat(StandardArgs.FLOAT_VAL.get(), is(10f)); } + }, + "float", "10.0"); + test(StandardArgs.class, + new Command() { + @Override public void execute() { assertThat(StandardArgs.DOUBLE_VAL.get(), is(10d)); } + }, + "double", "10.0"); + test(StandardArgs.class, + new Command() { + @Override public void execute() { assertThat(StandardArgs.BOOL.get(), is(true)); } + }, + "bool", "true"); + test(StandardArgs.class, + new Command() { + @Override public void execute() { assertThat(StandardArgs.BOOL.get(), is(true)); } + }, + "bool", ""); + test(StandardArgs.class, + new Command() { + @Override public void execute() { + assertThat(StandardArgs.REGEX.get().matcher("jack").matches(), is(true)); + } + }, + "regex", ".*ack$"); + test(StandardArgs.class, + new Command() { + @Override public void execute() { assertThat(StandardArgs.BOOL.get(), is(false)); } + }, + "no_bool", ""); + test(StandardArgs.class, + new Command() { + @Override public void execute() { assertThat(StandardArgs.BOOL.get(), is(true)); } + }, + "no_bool", "false"); + test(StandardArgs.class, + new Command() { + @Override public void execute() { + assertThat(StandardArgs.TIME_AMOUNT.get(), is(Amount.of(100L, Time.SECONDS))); + } + }, + "time_amount", "100secs"); + test(StandardArgs.class, + new Command() { + @Override public void execute() { + assertThat(StandardArgs.DATA_AMOUNT.get(), is(Amount.of(1L, Data.Gb))); + } + }, + "data_amount", "1Gb"); + test(StandardArgs.class, + new Command() { + @Override public void execute() { + assertThat(StandardArgs.RANGE.get(), is(com.google.common.collect.Range.closed(1, 5))); + } + }, + "range", "1-5"); + + resetArgs(StandardArgs.class); + assertTrue(parse(StandardArgs.class, "1mins", "2secs")); + assertEquals(ImmutableList.builder() + .add(Amount.of(60L, Time.SECONDS)) + .add(Amount.of(2L, Time.SECONDS)).build(), StandardArgs.POSITIONAL.get()); + } + + public static class Name { + private final String name; + + public Name(String name) { + this.name = MorePreconditions.checkNotBlank(name); + } + + public String getName() { + return name; + } + + @Override + public int hashCode() { + return this.name.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof Name) && name.equals(((Name) obj).name); + } + } + + @ArgParser + public static class NameParser extends NonParameterizedTypeParser<Name> { + @Override public Name doParse(String raw) { + return new Name(raw); + } + } + + public static class MeaningOfLife { + private final Long answer; + + public MeaningOfLife(Long answer) { + this.answer = Preconditions.checkNotNull(answer); + } + + @Override + public int hashCode() { + return this.answer.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof MeaningOfLife) && answer.equals(((MeaningOfLife) obj).answer); + } + } + + public static class Monty extends NonParameterizedTypeParser<MeaningOfLife> { + @Override public MeaningOfLife doParse(String raw) { + return new MeaningOfLife(42L); + } + } + + public static class CustomArgs { + @CmdLine(name = "custom1", help = "help") + static final Arg<Name> NAME_VAL = Arg.create(new Name("jim")); + + @CmdLine(name = "custom2", help = "help", parser = Monty.class) + static final Arg<MeaningOfLife> MEANING_VAL = Arg.create(new MeaningOfLife(13L)); + } + + @Test + public void testCustomArgs() { + test(CustomArgs.class, + new Command() { + @Override public void execute() { + assertThat(CustomArgs.NAME_VAL.get(), is(new Name("jane"))); + } + }, "custom1", "jane"); + test(CustomArgs.class, + new Command() { + @Override public void execute() { + assertThat(CustomArgs.MEANING_VAL.get(), is(new MeaningOfLife(42L))); + } + }, "custom2", "jim"); + } + + @Test + public void testHelp() { + assertFalse(parse(StandardArgs.class, "-h")); + assertFalse(parse(StandardArgs.class, "-help")); + } + + @Test + public void testAllowsEmptyString() { + parse(StandardArgs.class, "-string="); + assertThat(StandardArgs.STRING_VAL.get(), is("")); + + resetArgs(StandardArgs.class); + + parse(StandardArgs.class, "-string=''"); + assertThat(StandardArgs.STRING_VAL.get(), is("")); + + resetArgs(StandardArgs.class); + + parse(StandardArgs.class, "-string=\"\""); + assertThat(StandardArgs.STRING_VAL.get(), is("")); + } + + public static class CollectionArgs { + @CmdLine(name = "stringList", help = "help") + static final Arg<List<String>> STRING_LIST = Arg.create(null); + @CmdLine(name = "intList", help = "help") + static final Arg<List<Integer>> INT_LIST = Arg.create(null); + @CmdLine(name = "stringSet", help = "help") + static final Arg<Set<String>> STRING_SET = Arg.create(null); + @CmdLine(name = "intSet", help = "help") + static final Arg<Set<Integer>> INT_SET = Arg.create(null); + @CmdLine(name = "stringStringMap", help = "help") + static final Arg<Map<String, String>> STRING_STRING_MAP = Arg.create(null); + @CmdLine(name = "intIntMap", help = "help") + static final Arg<Map<Integer, Integer>> INT_INT_MAP = Arg.create(null); + @CmdLine(name = "stringIntMap", help = "help") + static final Arg<Map<String, Integer>> STRING_INT_MAP = Arg.create(null); + @CmdLine(name = "intStringMap", help = "help") + static final Arg<Map<Integer, String>> INT_STRING_MAP = Arg.create(null); + @CmdLine(name = "stringStringPair", help = "help") + static final Arg<Pair<String, String>> STRING_STRING_PAIR = Arg.create(null); + @CmdLine(name = "intIntPair", help = "help") + static final Arg<Pair<Integer, Integer>> INT_INT_PAIR = Arg.create(null); + @CmdLine(name = "stringTimeAmountPair", help = "help") + static final Arg<Pair<String, Amount<Long, Time>>> STRING_TIME_AMOUNT_PAIR = Arg.create(null); + } + + @Test + public void testCollectionArgs() { + test(CollectionArgs.class, + new Command() { + @Override public void execute() { + assertThat(CollectionArgs.STRING_LIST.get(), is(Arrays.asList("a", "b", "c", "d"))); + } + }, + "stringList", "a,b,c,d"); + test(CollectionArgs.class, + new Command() { + @Override public void execute() { + assertThat(CollectionArgs.INT_LIST.get(), is(Arrays.asList(1, 2, 3, 4))); + } + }, + "intList", "1, 2, 3, 4"); + test(CollectionArgs.class, + new Command() { + @Override public void execute() { + Set<String> expected = ImmutableSet.of("a", "b", "c", "d"); + assertThat(CollectionArgs.STRING_SET.get(), is(expected)); + } + }, + "stringSet", "a,b,c,d"); + test(CollectionArgs.class, + new Command() { + @Override public void execute() { + Set<Integer> expected = ImmutableSet.of(1, 2, 3, 4); + assertThat(CollectionArgs.INT_SET.get(), is(expected)); + } + }, + "intSet", "1, 2, 3, 4"); + test(CollectionArgs.class, + new Command() { + @Override public void execute() { + Map<String, String> expected = ImmutableMap.of("a", "b", "c", "d", "e", "f", "g", "h"); + assertThat(CollectionArgs.STRING_STRING_MAP.get(), is(expected)); + } + }, + "stringStringMap", "a=b, c=d, e=f, g=h"); + test(CollectionArgs.class, + new Command() { + @Override public void execute() { + Map<Integer, Integer> expected = ImmutableMap.of(1, 2, 3, 4, 5, 6, 7, 8); + assertThat(CollectionArgs.INT_INT_MAP.get(), is(expected)); + } + }, + "intIntMap", "1 = 2,3=4, 5=6 ,7=8"); + test(CollectionArgs.class, + new Command() { + @Override public void execute() { + Map<String, Integer> expected = ImmutableMap.of("a", 1, "b", 2, "c", 3, "d", 4); + assertThat(CollectionArgs.STRING_INT_MAP.get(), is(expected)); + } + }, + "stringIntMap", "a=1 , b=2, c=3 ,d=4"); + test(CollectionArgs.class, + new Command() { + @Override public void execute() { + Map<Integer, String> expected = ImmutableMap.of(1, "1", 2, "2", 3, "3", 4, "4"); + assertThat(CollectionArgs.INT_STRING_MAP.get(), is(expected)); + } + }, + "intStringMap", " 1=1 , 2=2, 3=3,4=4"); + test(CollectionArgs.class, + new Command() { + @Override public void execute() { + assertThat(CollectionArgs.STRING_STRING_PAIR.get(), is(Pair.of("foo", "bar"))); + } + }, + "stringStringPair", "foo , bar"); + test(CollectionArgs.class, + new Command() { + @Override public void execute() { + assertThat(CollectionArgs.INT_INT_PAIR.get(), is(Pair.of(10, 20))); + } + }, + "intIntPair", "10 ,20"); + test(CollectionArgs.class, + new Command() { + @Override public void execute() { + assertThat(CollectionArgs.STRING_TIME_AMOUNT_PAIR.get(), + is(Pair.of("fred", Amount.of(42L, Time.MINUTES)))); + } + }, + "stringTimeAmountPair", "fred ,42mins"); + test(CollectionArgs.class, + new Command() { + @Override public void execute() { + CollectionArgs.STRING_TIME_AMOUNT_PAIR.get(); + } + }, + true, "stringTimeAmountPair", "george,1MB"); + + } + + static class Serializable1 implements Serializable { } + static class Serializable2 implements Serializable { } + + public static class WildcardArgs { + @CmdLine(name = "class", help = "help") + static final Arg<? extends Class<? extends Serializable>> CLAZZ = + Arg.create(Serializable1.class); + @CmdLine(name = "classList1", help = "help") + static final Arg<List<Class<? extends Serializable>>> CLASS_LIST_1 = Arg.create(null); + @CmdLine(name = "classList2", help = "help") + static final Arg<List<? extends Class<? extends Serializable>>> CLASS_LIST_2 = Arg.create(null); + } + + @Test + public void testWildcardArgs() { + test(WildcardArgs.class, + new Command() { + @Override public void execute() { + assertSame(Serializable2.class, WildcardArgs.CLAZZ.get()); + } + }, + "class", Serializable2.class.getName()); + + test(WildcardArgs.class, + new Command() { + @Override public void execute() { + WildcardArgs.CLAZZ.get(); + } + }, + true, "class", Runnable.class.getName()); + + test(WildcardArgs.class, + new Command() { + @Override public void execute() { + assertEquals(ImmutableList.of(Serializable1.class, Serializable2.class), + WildcardArgs.CLASS_LIST_1.get()); + } + }, + "classList1", Serializable1.class.getName() + "," + Serializable2.class.getName()); + + test(WildcardArgs.class, + new Command() { + @Override public void execute() { + assertEquals(ImmutableList.of(Serializable2.class), WildcardArgs.CLASS_LIST_2.get()); + } + }, + "classList2", Serializable2.class.getName()); + + test(WildcardArgs.class, + new Command() { + @Override public void execute() { + WildcardArgs.CLASS_LIST_2.get(); + } + }, + true, "classList2", Serializable1.class.getName() + "," + Runnable.class.getName()); + } + + @Target(FIELD) + @Retention(RUNTIME) + public static @interface Equals { + String value(); + } + + @VerifierFor(Equals.class) + public static class SameName implements Verifier<Name> { + @Override + public void verify(Name value, Annotation annotation) { + Preconditions.checkArgument(getValue(annotation).equals(value.getName())); + } + + @Override + public String toString(Class<? extends Name> argType, Annotation annotation) { + return "name = " + getValue(annotation); + } + + private String getValue(Annotation annotation) { + return ((Equals) annotation).value(); + } + } + + public static class VerifyArgs { + @Equals("jake") @CmdLine(name = "custom", help = "help") + static final Arg<Name> CUSTOM_VAL = Arg.create(new Name("jake")); + @NotEmpty @CmdLine(name = "string", help = "help") + static final Arg<String> STRING_VAL = Arg.create("string"); + @NotEmpty @CmdLine(name = "optional_string", help = "help") + static final Arg<String> OPTIONAL_STRING_VAL = Arg.create(null); + @Positive @CmdLine(name = "int", help = "help") + static final Arg<Integer> INT_VAL = Arg.create(1); + @NotNegative @CmdLine(name = "long", help = "help") + static final Arg<Long> LONG_VAL = Arg.create(0L); + @Range(lower = 10, upper = 20) @CmdLine(name = "float", help = "help") + static final Arg<Float> FLOAT_VAL = Arg.create(10F); + @CmdLine(name = "double", help = "help") + static final Arg<Double> DOUBLE_VAL = Arg.create(0D); + @CmdLine(name = "bool", help = "help") + static final Arg<Boolean> BOOL = Arg.create(false); + @CmdLine(name = "arg_without_default", help = "help") + static final Arg<Boolean> ARG_WITHOUT_DEFAULT = Arg.create(); + } + + @Test + public void testEnforcesConstraints() { + test(VerifyArgs.class, + new Command() { + @Override public void execute() { + assertThat(VerifyArgs.STRING_VAL.get(), is("newstring")); + assertThat(VerifyArgs.OPTIONAL_STRING_VAL.get(), nullValue(String.class)); + } + }, + "string", "newstring"); + + testFails(VerifyArgs.class, "custom", "jane"); + testFails(VerifyArgs.class, "string", ""); + testFails(VerifyArgs.class, "optional_string", ""); + testFails(VerifyArgs.class, "int", "0"); + testFails(VerifyArgs.class, "long", "-1"); + + test(VerifyArgs.class, + new Command() { + @Override public void execute() { + assertThat(VerifyArgs.FLOAT_VAL.get(), is(10.5f)); + } + }, + "float", "10.5"); + testFails(VerifyArgs.class, "float", "9"); + } + + @Test + public void testJoinKeysToValues() { + assertThat(ArgScanner.joinKeysToValues(Arrays.asList("")), is(Arrays.asList(""))); + assertThat(ArgScanner.joinKeysToValues(Arrays.asList("-a", "b", "-c", "-d")), + is(Arrays.asList("-a=b", "-c", "-d"))); + assertThat(ArgScanner.joinKeysToValues(Arrays.asList("-a='b'", "-c", "-d", "'e'")), + is(Arrays.asList("-a='b'", "-c", "-d='e'"))); + assertThat(ArgScanner.joinKeysToValues(Arrays.asList("-a=-b", "c", "-d", "\"e\"")), + is(Arrays.asList("-a=-b", "c", "-d=\"e\""))); + } + + public static class ShortHelpArg { + @CmdLine(name = "h", help = "help") + static final Arg<String> SHORT_HELP = Arg.create("string"); + } + + @Test(expected = IllegalArgumentException.class) + public void testShortHelpReserved() { + parse(ShortHelpArg.class); + } + + public static class LongHelpArg { + @CmdLine(name = "help", help = "help") + static final Arg<String> LONG_HELP = Arg.create("string"); + } + + @Test(expected = IllegalArgumentException.class) + public void testLongHelpReserved() { + parse(LongHelpArg.class); + } + + public static class DuplicateNames { + @CmdLine(name = "string", help = "help") static final Arg<String> STRING_1 = Arg.create(); + @CmdLine(name = "string", help = "help") static final Arg<String> STRING_2 = Arg.create(); + } + + @Test(expected = IllegalArgumentException.class) + public void testRejectsDuplicates() { + parse(DuplicateNames.class, "-string-str"); + } + + public static class OneRequired { + @CmdLine(name = "string1", help = "help") + static final Arg<String> STRING_1 = Arg.create(null); + @NotNull @CmdLine(name = "string2", help = "help") + static final Arg<String> STRING_2 = Arg.create(null); + } + + @Test + public void testRequiredProvided() { + parse(OneRequired.class, "-string2=blah"); + } + + @Test(expected = IllegalArgumentException.class) + public void testMissingRequired() { + parse(OneRequired.class, "-string1=blah"); + } + + @Test(expected = IllegalArgumentException.class) + public void testUnrecognizedArg() { + parse(OneRequired.class, "-string2=blah", "-string3=blah"); + } + + public static class NameClashA { + @CmdLine(name = "string", help = "help") + static final Arg<String> STRING = Arg.create(null); + @CmdLine(name = "boolean", help = "help") + static final Arg<Boolean> BOOLEAN = Arg.create(true); + } + + public static class NameClashB { + @CmdLine(name = "string", help = "help") + static final Arg<String> STRING_1 = Arg.create(null); + @CmdLine(name = "boolean", help = "help") + static final Arg<Boolean> BOOLEAN_1 = Arg.create(true); + } + + @Test(expected = IllegalArgumentException.class) + public void testDisallowsShortNameOnArgCollision() { + parse(ImmutableList.of(NameClashA.class, NameClashB.class), "-string=blah"); + } + + @Test(expected = IllegalArgumentException.class) + public void testDisallowsShortNegNameOnArgCollision() { + parse(ImmutableList.of(NameClashA.class, NameClashB.class), "-no_boolean"); + } + + @Test + public void testAllowsCanonicalNameOnArgCollision() { + // TODO(William Farner): Fix. + parse(ImmutableList.of(NameClashA.class, NameClashB.class), + "-" + NameClashB.class.getCanonicalName() + ".string=blah"); + } + + @Test + public void testAllowsCanonicalNegNameOnArgCollision() { + parse(ImmutableList.of(NameClashA.class, NameClashB.class), + "-" + NameClashB.class.getCanonicalName() + ".no_boolean"); + } + + public static class AmountContainer { + @CmdLine(name = "time_amount", help = "help") + static final Arg<Amount<Integer, Time>> TIME_AMOUNT = Arg.create(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testBadUnitType() { + parse(ImmutableList.of(AmountContainer.class), "-time_amount=1Mb"); + } + + @Test(expected = IllegalArgumentException.class) + public void testUnrecognizedUnitType() { + parse(ImmutableList.of(AmountContainer.class), "-time_amount=1abcd"); + } + + static class Main1 { + @Positional(help = "halp") + static final Arg<List<String>> NAMES = Arg.create(null); + } + + static class Main2 { + @Positional(help = "halp") + static final Arg<List<List<String>>> ROSTERS = Arg.create(null); + } + + static class Main3 { + @Positional(help = "halp") + static final Arg<List<Double>> PERCENTILES = Arg.create(null); + + @Positional(help = "halp") + static final Arg<List<File>> FILES = Arg.create(null); + } + + private void resetMainArgs() { + resetArgs(Main1.class); + resetArgs(Main2.class); + resetArgs(Main3.class); + } + + @Test + public void testMultiplePositionalsFails() { + // Indivdually these should work. + + resetMainArgs(); + assertTrue(parse(Main1.class, "jack,jill", "laurel,hardy")); + assertEquals(ImmutableList.of("jack,jill", "laurel,hardy"), + ImmutableList.copyOf(Main1.NAMES.get())); + + resetMainArgs(); + assertTrue(parse(Main2.class, "jack,jill", "laurel,hardy")); + assertEquals( + ImmutableList.of( + ImmutableList.of("jack", "jill"), + ImmutableList.of("laurel", "hardy")), + ImmutableList.copyOf(Main2.ROSTERS.get())); + + // But if combined in the same class or across classes the @Positional is ambiguous and we + // should fail fast. + + resetMainArgs(); + try { + parse(ImmutableList.of(Main1.class, Main2.class), "jack,jill", "laurel,hardy"); + fail("Expected more than 1 in-scope @Positional Arg List to trigger a failure."); + } catch (IllegalArgumentException e) { + // expected + } + + resetMainArgs(); + try { + parse(Main3.class, "50", "90", "99", "99.9"); + fail("Expected more than 1 in-scope @Positional Arg List to trigger a failure."); + } catch (IllegalArgumentException e) { + // expected + } + } + + // TODO(William Farner): Do we want to support nested parameterized args? If so, need to define a + // syntax for that and build it in. + // e.g. List<List<Integer>>, List<Pair<String, String>> + + private static void testFails(Class<?> scope, String arg, String value) { + test(scope, null, true, arg, value); + } + + private static void test(Class<?> scope, Command validate, String arg, String value) { + test(scope, validate, false, arg, value); + } + + private static void test(Class<?> scope, Command validate, boolean expectFails, String arg, + String value) { + String canonicalName = scope.getCanonicalName() + "." + arg; + + if (value.isEmpty()) { + testValidate(scope, validate, expectFails, String.format("-%s", arg)); + testValidate(scope, validate, expectFails, String.format("-%s", canonicalName)); + } else { + testValidate(scope, validate, expectFails, String.format("-%s=%s", arg, value)); + testValidate(scope, validate, expectFails, String.format("-%s=%s", canonicalName, value)); + testValidate(scope, validate, expectFails, String.format("-%s='%s'", arg, value)); + testValidate(scope, validate, expectFails, String.format("-%s='%s'", canonicalName, value)); + testValidate(scope, validate, expectFails, String.format("-%s=\"%s\"", arg, value)); + testValidate(scope, validate, expectFails, String.format("-%s=\"%s\"", canonicalName, value)); + testValidate(scope, validate, expectFails, String.format("-%s", arg), value); + testValidate(scope, validate, expectFails, String.format("-%s", canonicalName), value); + testValidate(scope, validate, expectFails, + String.format("-%s", arg), String.format("'%s'", value)); + testValidate(scope, validate, expectFails, + String.format("-%s", canonicalName), String.format("'%s'", value)); + testValidate(scope, validate, expectFails, String.format("-%s \"%s\"", arg, value)); + testValidate(scope, validate, expectFails, String.format("-%s \"%s\"", canonicalName, value)); + testValidate(scope, validate, expectFails, + String.format("-%s", arg), String.format("%s", value)); + testValidate(scope, validate, expectFails, + String.format("-%s", canonicalName), String.format("%s", value)); + } + } + + private static void testValidate(Class<?> scope, Command validate, boolean expectFails, + String... args) { + resetArgs(scope); + IllegalArgumentException exception = null; + try { + assertTrue(parse(scope, args)); + } catch (IllegalArgumentException e) { + exception = e; + } + + if (!expectFails && exception != null) { + throw exception; + } + if (expectFails && exception == null) { + fail("Expected exception."); + } + + if (validate != null) { + validate.execute(); + } + resetArgs(scope); + } + + private static void resetArgs(Class<?> scope) { + for (Field field : scope.getDeclaredFields()) { + if (Arg.class.isAssignableFrom(field.getType()) && Modifier.isStatic(field.getModifiers())) { + try { + ((Arg) field.get(null)).reset(); + } catch (IllegalAccessException e) { + fail(e.getMessage()); + } + } + } + } + + private static boolean parse(final Class<?> scope, String... args) { + return parse(ImmutableList.of(scope), args); + } + + private static boolean parse(Iterable<? extends Class<?>> scopes, String... args) { + Predicate<Field> filter = Predicates.or(Iterables.transform(scopes, TO_SCOPE_PREDICATE)); + PrintStream devNull = new PrintStream(ByteStreams.nullOutputStream()); + return new ArgScanner(devNull).parse(filter, Arrays.asList(args)); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/test/java/com/twitter/common/args/ArgTest.java ---------------------------------------------------------------------- diff --git a/commons/src/test/java/com/twitter/common/args/ArgTest.java b/commons/src/test/java/com/twitter/common/args/ArgTest.java new file mode 100644 index 0000000..3a20eca --- /dev/null +++ b/commons/src/test/java/com/twitter/common/args/ArgTest.java @@ -0,0 +1,37 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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.twitter.common.args; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +public class ArgTest { + + @Test + public void testSetAfterGet() { + Arg<Boolean> arg = new Arg<Boolean>(false); + arg.get(); + try { + arg.set(true); + fail("Expected set after get to throw"); + } catch (IllegalStateException e) { + assertFalse(arg.get()); + } + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/test/java/com/twitter/common/args/ArgsTest.java ---------------------------------------------------------------------- diff --git a/commons/src/test/java/com/twitter/common/args/ArgsTest.java b/commons/src/test/java/com/twitter/common/args/ArgsTest.java new file mode 100644 index 0000000..d6a8b98 --- /dev/null +++ b/commons/src/test/java/com/twitter/common/args/ArgsTest.java @@ -0,0 +1,76 @@ +// ================================================================================================= +// Copyright 2013 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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.twitter.common.args; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import com.google.common.collect.ImmutableList; + +import org.junit.Test; + +import com.twitter.common.args.constraints.NotEmpty; +import com.twitter.common.args.constraints.Range; + +import static junit.framework.Assert.assertEquals; + +public class ArgsTest { + private static class App { + @CmdLine(name = "db", help = "help") + private static final Arg<File> DB = Arg.create(); + + @NotEmpty + @CmdLine(name = "name", help = "help") + private final Arg<String> name = Arg.create(); + + @Positional(help = "help") + private final Arg<List<Integer>> values = Arg.create(); + } + + @Test + public void testMixed() throws IOException { + App app = new App(); + + new ArgScanner().parse(Args.from(ArgFilters.selectClass(App.class), app), + ImmutableList.of("-name=bob", "-db=fred", "1", "137")); + + assertEquals(new File("fred"), App.DB.get()); + assertEquals("bob", app.name.get()); + assertEquals(ImmutableList.of(1, 137), app.values.get()); + } + + @Test + public void testReentrance() throws IOException { + class InnerApp { + @Range(lower = 0.0, upper = 1.0) + @CmdLine(name = "level", help = "help") + private final Arg<Double> level = Arg.create(); + } + + InnerApp app1 = new InnerApp(); + InnerApp app2 = new InnerApp(); + + new ArgScanner().parse(Args.from(ArgFilters.selectClass(InnerApp.class), app1), + ImmutableList.of("-level=0.5")); + new ArgScanner().parse(Args.from(ArgFilters.selectClass(InnerApp.class), app2), + ImmutableList.of("-level=0.00729")); + + assertEquals(0.5, app1.level.get(), 0.00001); + assertEquals(0.00729, app2.level.get(), 0.00001); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/test/java/com/twitter/common/args/OptionInfoTest.java ---------------------------------------------------------------------- diff --git a/commons/src/test/java/com/twitter/common/args/OptionInfoTest.java b/commons/src/test/java/com/twitter/common/args/OptionInfoTest.java new file mode 100644 index 0000000..166846d --- /dev/null +++ b/commons/src/test/java/com/twitter/common/args/OptionInfoTest.java @@ -0,0 +1,111 @@ +// ================================================================================================= +// Copyright 2015 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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.twitter.common.args; + +import java.io.File; +import java.util.List; + +import com.google.common.base.Charsets; +import com.google.common.collect.ImmutableList; +import com.google.common.io.Files; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import org.junit.rules.TemporaryFolder; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +public class OptionInfoTest { + private static class App { + @CmdLine(name = "files", help = "help.", argFile = true) + private final Arg<List<File>> files = Arg.<List<File>>create(ImmutableList.<File>of()); + + @CmdLine(name = "flag", help = "help.") + private final Arg<Boolean> flag = Arg.create(); + } + + @Rule + public TemporaryFolder tmpDir = new TemporaryFolder(); + private App app; + + @Before + public void setUp() throws Exception { + app = new App(); + } + + @Test + public void testArgumentFilesCreateFromField() throws Exception { + OptionInfo optionInfo = OptionInfo.createFromField(App.class.getDeclaredField("files"), app); + assertEquals("files", optionInfo.getName()); + assertEquals( + String.format(OptionInfo.ARG_FILE_HELP_TEMPLATE, "help.", "files", "files"), + optionInfo.getHelp()); + assertTrue(optionInfo.argFile()); + assertEquals("com.twitter.common.args.OptionInfoTest.App.files", + optionInfo.getCanonicalName()); + } + + @Test + public void testArgumentFilesRegularFormat() throws Exception { + new ArgScanner().parse(Args.from(ArgFilters.selectClass(App.class), app), + ImmutableList.of("-files=1.txt,2.txt")); + assertEquals( + ImmutableList.of(new File("1.txt"), new File("2.txt")), + app.files.get()); + } + + @Test(expected = IllegalArgumentException.class) + public void testArgumentFilesArgFileFormatEmptyFileName() throws Exception { + new ArgScanner().parse(Args.from(ArgFilters.selectClass(App.class), app), + ImmutableList.of("-files=@")); + } + + @Test(expected = IllegalArgumentException.class) + public void testArgumentFilesArgFileFormatFileNotExist() throws Exception { + new ArgScanner().parse(Args.from(ArgFilters.selectClass(App.class), app), + ImmutableList.of("-files=@file_does_not_exist.txt")); + } + + @Test + public void testArgumentFilesArgFileFormat() throws Exception { + File argfile = tmpDir.newFile(); + // Note the '\n' at the end. Some editors auto add a newline at the end so + // make sure our arg scanner and parser can deal with this. + Files.write("1.txt,2.txt\n", argfile, Charsets.UTF_8); + new ArgScanner().parse(Args.from(ArgFilters.selectClass(App.class), app), + ImmutableList.of("-files=@" + argfile.getCanonicalPath())); + assertEquals( + ImmutableList.of(new File("1.txt"), new File("2.txt")), + app.files.get()); + } + + @Test + public void testArgumentFlagCreateFromField() throws Exception { + OptionInfo optionInfo = OptionInfo.createFromField(App.class.getDeclaredField("flag"), app); + assertEquals("flag", optionInfo.getName()); + assertEquals("help.", optionInfo.getHelp()); + assertFalse(optionInfo.argFile()); + assertEquals("com.twitter.common.args.OptionInfoTest.App.flag", optionInfo.getCanonicalName()); + assertEquals("no_flag", optionInfo.getNegatedName()); + assertEquals( + "com.twitter.common.args.OptionInfoTest.App.no_flag", optionInfo.getCanonicalNegatedName()); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/test/java/com/twitter/common/args/ParsersTest.java ---------------------------------------------------------------------- diff --git a/commons/src/test/java/com/twitter/common/args/ParsersTest.java b/commons/src/test/java/com/twitter/common/args/ParsersTest.java new file mode 100644 index 0000000..f3da00d --- /dev/null +++ b/commons/src/test/java/com/twitter/common/args/ParsersTest.java @@ -0,0 +1,84 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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.twitter.common.args; + +import com.google.common.collect.ImmutableMap; +import com.google.common.reflect.TypeToken; + +import org.junit.Before; +import org.junit.Test; + +import com.twitter.common.args.apt.Configuration.ParserInfo; +import com.twitter.common.args.parsers.NonParameterizedTypeParser; +import com.twitter.common.args.parsers.PairParser; +import com.twitter.common.collections.Pair; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +public class ParsersTest { + + private Parsers defaultParsers; + + @Before + public void setUp() { + defaultParsers = + new Parsers(ImmutableMap.<Class<?>, Parser<?>>of( + String.class, new StringParser(), + Pair.class, new PairParser())); + } + + @Test + public void testParseTypeFamily() { + assertNotNull(defaultParsers.get(TypeToken.of(String.class))); + + class Credentials extends Pair<String, String> { + public Credentials(String first, String second) { + super(first, second); + } + } + Parser parser = defaultParsers.get(TypeToken.of(Credentials.class)); + assertNotNull(parser); + assertSame(parser, defaultParsers.get(TypeToken.of(Pair.class))); + } + + @Test(expected = IllegalArgumentException.class) + public void testNoParser() { + class NoParserForMe { } + assertNull(defaultParsers.get(TypeToken.of(NoParserForMe.class))); + } + + static class StringParser extends NonParameterizedTypeParser<Integer> { + @Override public Integer doParse(String raw) throws IllegalArgumentException { + return raw.length(); + } + } + + @Test + public void testNonPublicParsers() { + @SuppressWarnings("unchecked") + Parser<Integer> parser = (Parser<Integer>) + Parsers.INFO_TO_PARSER.apply( + new ParserInfo(Integer.class.getName(), StringParser.class.getName())); + + assertEquals( + Integer.valueOf(42), + parser.parse(null, null, "themeaningoflifeisfortytwointhebookbyadams")); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/test/java/com/twitter/common/args/argfilterstest/ArgsRoot.java ---------------------------------------------------------------------- diff --git a/commons/src/test/java/com/twitter/common/args/argfilterstest/ArgsRoot.java b/commons/src/test/java/com/twitter/common/args/argfilterstest/ArgsRoot.java new file mode 100644 index 0000000..53b9abe --- /dev/null +++ b/commons/src/test/java/com/twitter/common/args/argfilterstest/ArgsRoot.java @@ -0,0 +1,32 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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.twitter.common.args.argfilterstest; + +import com.twitter.common.args.Arg; +import com.twitter.common.args.CmdLine; + +/** + * @author John Sirois + */ +public final class ArgsRoot { + @CmdLine(name = "args_root", help = "") + static final Arg<String> ARGS_ROOT = Arg.create(); + + private ArgsRoot() { + // Test class. + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/test/java/com/twitter/common/args/argfilterstest/subpackageA/ArgsA.java ---------------------------------------------------------------------- diff --git a/commons/src/test/java/com/twitter/common/args/argfilterstest/subpackageA/ArgsA.java b/commons/src/test/java/com/twitter/common/args/argfilterstest/subpackageA/ArgsA.java new file mode 100644 index 0000000..b26895c --- /dev/null +++ b/commons/src/test/java/com/twitter/common/args/argfilterstest/subpackageA/ArgsA.java @@ -0,0 +1,32 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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.twitter.common.args.argfilterstest.subpackageA; + +import com.twitter.common.args.Arg; +import com.twitter.common.args.CmdLine; + +/** + * @author John Sirois + */ +public final class ArgsA { + @CmdLine(name = "args_a", help = "") + static final Arg<String> ARGS_A = Arg.create(); + + private ArgsA() { + // Test class. + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/test/java/com/twitter/common/args/argfilterstest/subpackageA/subsubpackage1/ArgsA1.java ---------------------------------------------------------------------- diff --git a/commons/src/test/java/com/twitter/common/args/argfilterstest/subpackageA/subsubpackage1/ArgsA1.java b/commons/src/test/java/com/twitter/common/args/argfilterstest/subpackageA/subsubpackage1/ArgsA1.java new file mode 100644 index 0000000..9ec7686 --- /dev/null +++ b/commons/src/test/java/com/twitter/common/args/argfilterstest/subpackageA/subsubpackage1/ArgsA1.java @@ -0,0 +1,32 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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.twitter.common.args.argfilterstest.subpackageA.subsubpackage1; + +import com.twitter.common.args.Arg; +import com.twitter.common.args.CmdLine; + +/** + * @author John Sirois + */ +public final class ArgsA1 { + @CmdLine(name = "args_a1", help = "") + static final Arg<String> ARGS_A1 = Arg.create(); + + private ArgsA1() { + // Test class. + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/test/java/com/twitter/common/args/argfilterstest/subpackageB/ArgsB.java ---------------------------------------------------------------------- diff --git a/commons/src/test/java/com/twitter/common/args/argfilterstest/subpackageB/ArgsB.java b/commons/src/test/java/com/twitter/common/args/argfilterstest/subpackageB/ArgsB.java new file mode 100644 index 0000000..54a25ba --- /dev/null +++ b/commons/src/test/java/com/twitter/common/args/argfilterstest/subpackageB/ArgsB.java @@ -0,0 +1,32 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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.twitter.common.args.argfilterstest.subpackageB; + +import com.twitter.common.args.Arg; +import com.twitter.common.args.CmdLine; + +/** + * @author John Sirois + */ +public final class ArgsB { + @CmdLine(name = "args_b", help = "") + static final Arg<String> ARGS_B = Arg.create(); + + private ArgsB() { + // Test class. + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/test/java/com/twitter/common/args/argfilterstest/subpackageBwithSuffix/ArgsBWithSuffix.java ---------------------------------------------------------------------- diff --git a/commons/src/test/java/com/twitter/common/args/argfilterstest/subpackageBwithSuffix/ArgsBWithSuffix.java b/commons/src/test/java/com/twitter/common/args/argfilterstest/subpackageBwithSuffix/ArgsBWithSuffix.java new file mode 100644 index 0000000..5c9f12b --- /dev/null +++ b/commons/src/test/java/com/twitter/common/args/argfilterstest/subpackageBwithSuffix/ArgsBWithSuffix.java @@ -0,0 +1,32 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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.twitter.common.args.argfilterstest.subpackageBwithSuffix; + +import com.twitter.common.args.Arg; +import com.twitter.common.args.CmdLine; + +/** + * @author John Sirois + */ +public final class ArgsBWithSuffix { + @CmdLine(name = "args_b_with_suffix", help = "") + static final Arg<String> ARGS_B_WITH_SUFFIX = Arg.create(); + + private ArgsBWithSuffix() { + // Test class. + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/test/java/com/twitter/common/base/CachingSupplierTest.java ---------------------------------------------------------------------- diff --git a/commons/src/test/java/com/twitter/common/base/CachingSupplierTest.java b/commons/src/test/java/com/twitter/common/base/CachingSupplierTest.java new file mode 100644 index 0000000..627d30f --- /dev/null +++ b/commons/src/test/java/com/twitter/common/base/CachingSupplierTest.java @@ -0,0 +1,67 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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.twitter.common.base; + +import org.junit.Before; +import org.junit.Test; + +import com.twitter.common.quantity.Amount; +import com.twitter.common.quantity.Time; +import com.twitter.common.testing.easymock.EasyMockTest; +import com.twitter.common.util.testing.FakeClock; + +import static org.easymock.EasyMock.expect; +import static org.junit.Assert.assertEquals; + +/** + * @author William Farner + */ +public class CachingSupplierTest extends EasyMockTest { + + private static final Amount<Long, Time> ONE_SECOND = Amount.of(1L, Time.SECONDS); + + private Supplier<String> supplier; + private FakeClock clock; + private Supplier<String> cache; + + @Before + public void setUp() { + supplier = createMock(new Clazz<Supplier<String>>() { }); + clock = new FakeClock(); + cache = new CachingSupplier<String>(supplier, ONE_SECOND, clock); + } + + @Test + public void testCaches() { + expect(supplier.get()).andReturn("foo"); + expect(supplier.get()).andReturn("bar"); + + control.replay(); + + assertEquals("foo", cache.get()); + assertEquals("foo", cache.get()); + + clock.advance(Amount.of(999L, Time.MILLISECONDS)); + assertEquals("foo", cache.get()); + + clock.advance(Amount.of(1L, Time.MILLISECONDS)); + assertEquals("foo", cache.get()); + + clock.advance(Amount.of(1L, Time.MILLISECONDS)); + assertEquals("bar", cache.get()); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/test/java/com/twitter/common/base/ClosuresTest.java ---------------------------------------------------------------------- diff --git a/commons/src/test/java/com/twitter/common/base/ClosuresTest.java b/commons/src/test/java/com/twitter/common/base/ClosuresTest.java new file mode 100644 index 0000000..f2d0adb --- /dev/null +++ b/commons/src/test/java/com/twitter/common/base/ClosuresTest.java @@ -0,0 +1,176 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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.twitter.common.base; + +import java.io.IOException; + +import com.google.common.base.Function; +import com.google.common.base.Predicate; + +import org.easymock.EasyMock; +import org.junit.Test; + +import com.twitter.common.testing.easymock.EasyMockTest; + +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.expectLastCall; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; + +/** + * @author John Sirois + */ +public class ClosuresTest extends EasyMockTest { + + private static final Clazz<Closure<Integer>> INT_CLOSURE_CLZ = new Clazz<Closure<Integer>>() { }; + private static final Clazz<ExceptionalClosure<Integer, IOException>> EXC_INT_CLOSURE_CLZ = + new Clazz<ExceptionalClosure<Integer, IOException>>() { }; + + @Test(expected = NullPointerException.class) + public void testPreconditions() { + control.replay(); + + Closures.asFunction(null); + } + + @Test + public void testApply() throws IOException { + ExceptionalClosure<Integer, IOException> work = createMock(EXC_INT_CLOSURE_CLZ); + work.execute(1); + control.replay(); + + Function<Integer, Void> workFunction = Closures.asFunction(work); + workFunction.apply(1); + } + + static class Thrown extends RuntimeException { } + + @Test + public void testApplyThrows() throws IOException { + ExceptionalClosure<Integer, IOException> work = createMock(EXC_INT_CLOSURE_CLZ); + work.execute(1); + RuntimeException runtimeException = new Thrown(); + EasyMock.expectLastCall().andThrow(runtimeException); + control.replay(); + + Function<Integer, Void> workFunction = Closures.asFunction(work); + try { + workFunction.apply(1); + } catch (Thrown e) { + assertSame(runtimeException, e); + } + } + + @Test + public void testApplyThrowsTransparent() throws IOException { + Closure<Integer> work = createMock(INT_CLOSURE_CLZ); + work.execute(1); + RuntimeException runtimeException = new Thrown(); + EasyMock.expectLastCall().andThrow(runtimeException); + control.replay(); + + Function<Integer, Void> workFunction = Closures.asFunction(work); + try { + workFunction.apply(1); + } catch (Thrown e) { + assertSame(runtimeException, e); + } + } + + @Test + public void testCombine() { + Closure<Integer> work1 = createMock(INT_CLOSURE_CLZ); + Closure<Integer> work2 = createMock(INT_CLOSURE_CLZ); + + @SuppressWarnings("unchecked") // Needed because type information lost in vargs. + Closure<Integer> wrapper = Closures.combine(work1, work2); + + work1.execute(1); + work2.execute(1); + + work1.execute(2); + work2.execute(2); + + control.replay(); + + wrapper.execute(1); + wrapper.execute(2); + } + + @Test + public void testCombineOneThrows() { + Closure<Integer> work1 = createMock(INT_CLOSURE_CLZ); + Closure<Integer> work2 = createMock(INT_CLOSURE_CLZ); + Closure<Integer> work3 = createMock(INT_CLOSURE_CLZ); + + @SuppressWarnings("unchecked") // Needed because type information lost in vargs. + Closure<Integer> wrapper = Closures.combine(work1, work2, work3); + + work1.execute(1); + expectLastCall().andThrow(new Thrown()); + + work1.execute(2); + work2.execute(2); + expectLastCall().andThrow(new Thrown()); + + work1.execute(3); + work2.execute(3); + work3.execute(3); + expectLastCall().andThrow(new Thrown()); + + control.replay(); + + try { + wrapper.execute(1); + fail("Should have thrown."); + } catch (Thrown e) { + // Expected. + } + + try { + wrapper.execute(2); + fail("Should have thrown."); + } catch (Thrown e) { + // Expected. + } + + try { + wrapper.execute(3); + fail("Should have thrown."); + } catch (Thrown e) { + // Expected. + } + } + + @Test + public void testFilter() { + Predicate<Integer> filter = createMock(new Clazz<Predicate<Integer>>() { }); + Closure<Integer> work = createMock(INT_CLOSURE_CLZ); + + expect(filter.apply(1)).andReturn(true); + work.execute(1); + + expect(filter.apply(2)).andReturn(false); + + Closure<Integer> filtered = Closures.filter(filter, work); + + control.replay(); + + filtered.execute(1); + filtered.execute(2); + } +}
