/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.beam.sdk.transforms.reflect;
import static org.apache.beam.sdk.transforms.reflect.DoFnSignaturesTestUtils.analyzeProcessElementMethod;
import static org.apache.beam.sdk.transforms.reflect.DoFnSignaturesTestUtils.errors;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import java.util.List;
import org.apache.beam.sdk.coders.KvCoder;
import org.apache.beam.sdk.coders.StructuredCoder;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.DoFn.BoundedPerElement;
import org.apache.beam.sdk.transforms.DoFn.UnboundedPerElement;
import org.apache.beam.sdk.transforms.reflect.DoFnSignaturesTestUtils.AnonymousMethod;
import org.apache.beam.sdk.transforms.reflect.DoFnSignaturesTestUtils.FakeDoFn;
import org.apache.beam.sdk.transforms.splittabledofn.HasDefaultTracker;
import org.apache.beam.sdk.transforms.splittabledofn.RestrictionTracker;
import org.apache.beam.sdk.transforms.windowing.BoundedWindow;
import org.apache.beam.sdk.values.PCollection;
import org.apache.beam.sdk.values.TypeDescriptor;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Tests for {@link DoFnSignatures} focused on methods related to <a
* href="https://s.apache.org/splittable-do-fn">splittable</a> {@link DoFn}.
*/
@SuppressWarnings("unused")
@RunWith(JUnit4.class)
public class DoFnSignaturesSplittableDoFnTest {
@Rule public ExpectedException thrown = ExpectedException.none();
private static class SomeRestriction {}
private abstract static class SomeRestrictionTracker
implements RestrictionTracker<SomeRestriction> {}
private abstract static class SomeRestrictionCoder extends StructuredCoder<SomeRestriction> {}
@Test
public void testHasRestrictionTracker() throws Exception {
DoFnSignature.ProcessElementMethod signature =
analyzeProcessElementMethod(
new AnonymousMethod() {
private void method(
DoFn<Integer, String>.ProcessContext context, SomeRestrictionTracker tracker) {}
});
assertTrue(signature.isSplittable());
assertTrue(Iterables.any(signature.extraParameters(),
Predicates.instanceOf(DoFnSignature.Parameter.RestrictionTrackerParameter.class)));
assertEquals(SomeRestrictionTracker.class, signature.trackerT().getRawType());
}
@Test
public void testSplittableProcessElementMustNotHaveOtherParams() throws Exception {
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Illegal parameter");
thrown.expectMessage("BoundedWindow");
DoFnSignature.ProcessElementMethod signature =
analyzeProcessElementMethod(
new AnonymousMethod() {
private void method(
DoFn<Integer, String>.ProcessContext context,
SomeRestrictionTracker tracker,
BoundedWindow window) {}
});
}
@Test
public void testInfersBoundednessFromAnnotation() throws Exception {
class BaseSplittableFn extends DoFn<Integer, String> {
@ProcessElement
public void processElement(ProcessContext context, SomeRestrictionTracker tracker) {}
@GetInitialRestriction
public SomeRestriction getInitialRestriction(Integer element) {
return null;
}
@NewTracker
public SomeRestrictionTracker newTracker(SomeRestriction restriction) {
return null;
}
}
@BoundedPerElement
class BoundedSplittableFn extends BaseSplittableFn {}
@UnboundedPerElement
class UnboundedSplittableFn extends BaseSplittableFn {}
assertEquals(
PCollection.IsBounded.BOUNDED,
DoFnSignatures
.getSignature(BaseSplittableFn.class)
.isBoundedPerElement());
assertEquals(
PCollection.IsBounded.BOUNDED,
DoFnSignatures
.getSignature(BoundedSplittableFn.class)
.isBoundedPerElement());
assertEquals(
PCollection.IsBounded.UNBOUNDED,
DoFnSignatures
.getSignature(UnboundedSplittableFn.class)
.isBoundedPerElement());
}
@Test
public void testUnsplittableIsBounded() throws Exception {
class UnsplittableFn extends DoFn<Integer, String> {
@ProcessElement
public void process(ProcessContext context) {}
}
assertEquals(
PCollection.IsBounded.BOUNDED,
DoFnSignatures
.getSignature(UnsplittableFn.class)
.isBoundedPerElement());
}
@Test
public void testUnsplittableButDeclaresBounded() throws Exception {
@BoundedPerElement
class SomeFn extends DoFn<Integer, String> {
@ProcessElement
public void process(ProcessContext context) {}
}
thrown.expectMessage("Non-splittable, but annotated as @Bounded");
DoFnSignatures.getSignature(SomeFn.class);
}
@Test
public void testUnsplittableButDeclaresUnbounded() throws Exception {
@UnboundedPerElement
class SomeFn extends DoFn<Integer, String> {
@ProcessElement
public void process(ProcessContext context) {}
}
thrown.expectMessage("Non-splittable, but annotated as @Unbounded");
DoFnSignatures.getSignature(SomeFn.class);
}
/** Tests a splittable {@link DoFn} that defines all methods in their full form, correctly. */
@Test
public void testSplittableWithAllFunctions() throws Exception {
class GoodSplittableDoFn extends DoFn<Integer, String> {
@ProcessElement
public void processElement(
ProcessContext context, SomeRestrictionTracker tracker) {}
@GetInitialRestriction
public SomeRestriction getInitialRestriction(Integer element) {
return null;
}
@SplitRestriction
public void splitRestriction(
Integer element, SomeRestriction restriction, OutputReceiver<SomeRestriction> receiver) {}
@NewTracker
public SomeRestrictionTracker newTracker(SomeRestriction restriction) {
return null;
}
@GetRestrictionCoder
public SomeRestrictionCoder getRestrictionCoder() {
return null;
}
}
DoFnSignature signature = DoFnSignatures.getSignature(GoodSplittableDoFn.class);
assertEquals(SomeRestrictionTracker.class, signature.processElement().trackerT().getRawType());
assertTrue(signature.processElement().isSplittable());
assertEquals(
SomeRestriction.class, signature.getInitialRestriction().restrictionT().getRawType());
assertEquals(SomeRestriction.class, signature.splitRestriction().restrictionT().getRawType());
assertEquals(SomeRestrictionTracker.class, signature.newTracker().trackerT().getRawType());
assertEquals(SomeRestriction.class, signature.newTracker().restrictionT().getRawType());
assertEquals(SomeRestrictionCoder.class, signature.getRestrictionCoder().coderT().getRawType());
}
/**
* Tests a splittable {@link DoFn} that defines all methods in their full form, correctly, using
* generic types.
*/
@Test
public void testSplittableWithAllFunctionsGeneric() throws Exception {
class GoodGenericSplittableDoFn<RestrictionT, TrackerT, CoderT> extends DoFn<Integer, String> {
@ProcessElement
public void processElement(ProcessContext context, TrackerT tracker) {}
@GetInitialRestriction
public RestrictionT getInitialRestriction(Integer element) {
return null;
}
@SplitRestriction
public void splitRestriction(
Integer element, RestrictionT restriction, OutputReceiver<RestrictionT> receiver) {}
@NewTracker
public TrackerT newTracker(RestrictionT restriction) {
return null;
}
@GetRestrictionCoder
public CoderT getRestrictionCoder() {
return null;
}
}
DoFnSignature signature =
DoFnSignatures.getSignature(
new GoodGenericSplittableDoFn<
SomeRestriction, SomeRestrictionTracker, SomeRestrictionCoder>() {}.getClass());
assertEquals(SomeRestrictionTracker.class, signature.processElement().trackerT().getRawType());
assertTrue(signature.processElement().isSplittable());
assertEquals(
SomeRestriction.class, signature.getInitialRestriction().restrictionT().getRawType());
assertEquals(SomeRestriction.class, signature.splitRestriction().restrictionT().getRawType());
assertEquals(SomeRestrictionTracker.class, signature.newTracker().trackerT().getRawType());
assertEquals(SomeRestriction.class, signature.newTracker().restrictionT().getRawType());
assertEquals(SomeRestrictionCoder.class, signature.getRestrictionCoder().coderT().getRawType());
}
@Test
public void testSplittableMissingRequiredMethods() throws Exception {
class BadFn extends DoFn<Integer, String> {
@ProcessElement
public void process(ProcessContext context, SomeRestrictionTracker tracker) {}
}
thrown.expectMessage(
"Splittable, but does not define the following required methods: "
+ "[@GetInitialRestriction, @NewTracker]");
DoFnSignatures.getSignature(BadFn.class);
}
abstract class SomeDefaultTracker implements RestrictionTracker<RestrictionWithDefaultTracker> {}
abstract class RestrictionWithDefaultTracker
implements HasDefaultTracker<RestrictionWithDefaultTracker, SomeDefaultTracker> {}
@Test
public void testHasDefaultTracker() throws Exception {
class Fn extends DoFn<Integer, String> {
@ProcessElement
public void process(ProcessContext c, SomeDefaultTracker tracker) {}
@GetInitialRestriction
public RestrictionWithDefaultTracker getInitialRestriction(Integer element) {
return null;
}
}
DoFnSignature signature = DoFnSignatures.getSignature(Fn.class);
assertEquals(
SomeDefaultTracker.class, signature.processElement().trackerT().getRawType());
}
@Test
public void testRestrictionHasDefaultTrackerProcessUsesWrongTracker() throws Exception {
class Fn extends DoFn<Integer, String> {
@ProcessElement
public void process(ProcessContext c, SomeRestrictionTracker tracker) {}
@GetInitialRestriction
public RestrictionWithDefaultTracker getInitialRestriction(Integer element) {
return null;
}
}
thrown.expectMessage(
"Has tracker type SomeRestrictionTracker, but the DoFn's tracker type was inferred as ");
thrown.expectMessage("SomeDefaultTracker");
thrown.expectMessage(
"from restriction type RestrictionWithDefaultTracker "
+ "of @GetInitialRestriction method getInitialRestriction(Integer)");
DoFnSignatures.getSignature(Fn.class);
}
@Test
public void testNewTrackerReturnsWrongType() throws Exception {
class BadFn extends DoFn<Integer, String> {
@ProcessElement
public void process(ProcessContext context, SomeRestrictionTracker tracker) {}
@NewTracker
public void newTracker(SomeRestriction restriction) {}
@GetInitialRestriction
public SomeRestriction getInitialRestriction(Integer element) {
return null;
}
}
thrown.expectMessage(
"Returns void, but must return a subtype of RestrictionTracker<SomeRestriction>");
DoFnSignatures.getSignature(BadFn.class);
}
@Test
public void testGetInitialRestrictionMismatchesNewTracker() throws Exception {
class BadFn extends DoFn<Integer, String> {
@ProcessElement
public void process(ProcessContext context, SomeRestrictionTracker tracker) {}
@NewTracker
public SomeRestrictionTracker newTracker(SomeRestriction restriction) {
return null;
}
@GetInitialRestriction
public String getInitialRestriction(Integer element) {
return null;
}
}
thrown.expectMessage(
"getInitialRestriction(Integer): Uses restriction type String, but @NewTracker method");
thrown.expectMessage("newTracker(SomeRestriction) uses restriction type SomeRestriction");
DoFnSignatures.getSignature(BadFn.class);
}
@Test
public void testGetRestrictionCoderReturnsWrongType() throws Exception {
class BadFn extends DoFn<Integer, String> {
@ProcessElement
public void process(ProcessContext context, SomeRestrictionTracker tracker) {}
@NewTracker
public SomeRestrictionTracker newTracker(SomeRestriction restriction) {
return null;
}
@GetInitialRestriction
public SomeRestriction getInitialRestriction(Integer element) {
return null;
}
@GetRestrictionCoder
public KvCoder getRestrictionCoder() {
return null;
}
}
thrown.expectMessage(
"getRestrictionCoder() returns KvCoder which is not a subtype of Coder<SomeRestriction>");
DoFnSignatures.getSignature(BadFn.class);
}
@Test
public void testSplitRestrictionReturnsWrongType() throws Exception {
thrown.expectMessage(
"Third argument must be DoFn.OutputReceiver<SomeRestriction>, "
+ "but is DoFn.OutputReceiver<String>");
DoFnSignatures.analyzeSplitRestrictionMethod(
errors(),
TypeDescriptor.of(FakeDoFn.class),
new AnonymousMethod() {
void method(
Integer element, SomeRestriction restriction, DoFn.OutputReceiver<String> receiver) {}
}.getMethod(),
TypeDescriptor.of(Integer.class));
}
@Test
public void testSplitRestrictionWrongElementArgument() throws Exception {
class BadFn {
private List<SomeRestriction> splitRestriction(String element, SomeRestriction restriction) {
return null;
}
}
thrown.expectMessage("First argument must be the element type Integer");
DoFnSignatures.analyzeSplitRestrictionMethod(
errors(),
TypeDescriptor.of(FakeDoFn.class),
new AnonymousMethod() {
void method(
String element,
SomeRestriction restriction,
DoFn.OutputReceiver<SomeRestriction> receiver) {}
}.getMethod(),
TypeDescriptor.of(Integer.class));
}
@Test
public void testSplitRestrictionWrongNumArguments() throws Exception {
thrown.expectMessage("Must have exactly 3 arguments");
DoFnSignatures.analyzeSplitRestrictionMethod(
errors(),
TypeDescriptor.of(FakeDoFn.class),
new AnonymousMethod() {
private void method(
Integer element,
SomeRestriction restriction,
DoFn.OutputReceiver<SomeRestriction> receiver,
Object extra) {}
}.getMethod(),
TypeDescriptor.of(Integer.class));
}
@Test
public void testSplitRestrictionConsistentButWrongType() throws Exception {
class OtherRestriction {}
class BadFn extends DoFn<Integer, String> {
@ProcessElement
public void process(ProcessContext context, SomeRestrictionTracker tracker) {}
@NewTracker
public SomeRestrictionTracker newTracker(SomeRestriction restriction) {
return null;
}
@GetInitialRestriction
public SomeRestriction getInitialRestriction(Integer element) {
return null;
}
@DoFn.SplitRestriction
public void splitRestriction(
Integer element,
OtherRestriction restriction,
OutputReceiver<OtherRestriction> receiver) {}
}
thrown.expectMessage(
"getInitialRestriction(Integer): Uses restriction type SomeRestriction, "
+ "but @SplitRestriction method ");
thrown.expectMessage(
"splitRestriction(Integer, OtherRestriction, OutputReceiver) "
+ "uses restriction type OtherRestriction");
DoFnSignatures.getSignature(BadFn.class);
}
@Test
public void testUnsplittableMustNotDefineExtraMethods() throws Exception {
class BadFn extends DoFn<Integer, String> {
@ProcessElement
public void processElement(ProcessContext context) {}
@GetInitialRestriction
public SomeRestriction getInitialRestriction(Integer element) {
return null;
}
@SplitRestriction
public void splitRestriction(
Integer element, SomeRestriction restriction, OutputReceiver<SomeRestriction> receiver) {}
@NewTracker
public SomeRestrictionTracker newTracker(SomeRestriction restriction) {
return null;
}
@GetRestrictionCoder
public SomeRestrictionCoder getRestrictionCoder() {
return null;
}
}
thrown.expectMessage(
"Non-splittable, but defines methods: "
+ "[@GetInitialRestriction, @SplitRestriction, @NewTracker, @GetRestrictionCoder]");
DoFnSignatures.getSignature(BadFn.class);
}
@Test
public void testNewTrackerWrongNumArguments() throws Exception {
thrown.expectMessage("Must have a single argument");
DoFnSignatures.analyzeNewTrackerMethod(
errors(),
TypeDescriptor.of(FakeDoFn.class),
new AnonymousMethod() {
private SomeRestrictionTracker method(SomeRestriction restriction, Object extra) {
return null;
}
}.getMethod());
}
@Test
public void testNewTrackerInconsistent() throws Exception {
thrown.expectMessage(
"Returns SomeRestrictionTracker, but must return a subtype of RestrictionTracker<String>");
DoFnSignatures.analyzeNewTrackerMethod(
errors(),
TypeDescriptor.of(FakeDoFn.class),
new AnonymousMethod() {
private SomeRestrictionTracker method(String restriction) {
return null;
}
}.getMethod());
}
}