/* * 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; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.isOneOf; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertEquals; import com.google.common.collect.Lists; import java.util.ArrayList; import org.apache.beam.sdk.coders.CannotProvideCoderException; import org.apache.beam.sdk.coders.CoderRegistry; import org.apache.beam.sdk.coders.NullableCoder; import org.apache.beam.sdk.coders.VarLongCoder; import org.apache.beam.sdk.values.TimestampedValue; import org.joda.time.Instant; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** * Unit tests for {@link Latest.LatestFn}. * */ @RunWith(JUnit4.class) public class LatestFnTest { private static final Instant INSTANT = new Instant(100); private static final long VALUE = 100 * INSTANT.getMillis(); private static final TimestampedValue<Long> TV = TimestampedValue.of(VALUE, INSTANT); private static final TimestampedValue<Long> TV_MINUS_TEN = TimestampedValue.of(VALUE - 10, INSTANT.minus(10)); private static final TimestampedValue<Long> TV_PLUS_TEN = TimestampedValue.of(VALUE + 10, INSTANT.plus(10)); @Rule public final ExpectedException thrown = ExpectedException.none(); private final Latest.LatestFn<Long> fn = new Latest.LatestFn<>(); private final Instant baseTimestamp = Instant.now(); @Test public void testDefaultValue() { assertThat(fn.defaultValue(), nullValue()); } @Test public void testCreateAccumulator() { assertEquals(TimestampedValue.atMinimumTimestamp(null), fn.createAccumulator()); } @Test public void testAddInputInitialAdd() { TimestampedValue<Long> input = TV; assertEquals(input, fn.addInput(fn.createAccumulator(), input)); } @Test public void testAddInputMinTimestamp() { TimestampedValue<Long> input = TimestampedValue.atMinimumTimestamp(1234L); assertEquals(input, fn.addInput(fn.createAccumulator(), input)); } @Test public void testAddInputEarlierValue() { assertEquals(TV, fn.addInput(TV, TV_MINUS_TEN)); } @Test public void testAddInputLaterValue() { assertEquals(TV_PLUS_TEN, fn.addInput(TV, TV_PLUS_TEN)); } @Test public void testAddInputSameTimestamp() { TimestampedValue<Long> accum = TimestampedValue.of(100L, INSTANT); TimestampedValue<Long> input = TimestampedValue.of(200L, INSTANT); assertThat("Latest for values with the same timestamp is chosen arbitrarily", fn.addInput(accum, input), isOneOf(accum, input)); } @Test public void testAddInputNullAccumulator() { thrown.expect(NullPointerException.class); thrown.expectMessage("accumulator"); fn.addInput(null, TV); } @Test public void testAddInputNullInput() { thrown.expect(NullPointerException.class); thrown.expectMessage("input"); fn.addInput(TV, null); } @Test public void testAddInputNullValue() { TimestampedValue<Long> input = TimestampedValue.of(null, INSTANT.plus(10)); assertEquals("Null values are allowed", input, fn.addInput(TV, input)); } @Test public void testMergeAccumulatorsMultipleValues() { Iterable<TimestampedValue<Long>> accums = Lists.newArrayList( TV, TV_PLUS_TEN, TV_MINUS_TEN ); assertEquals(TV_PLUS_TEN, fn.mergeAccumulators(accums)); } @Test public void testMergeAccumulatorsSingleValue() { assertEquals(TV, fn.mergeAccumulators(Lists.newArrayList(TV))); } @Test public void testMergeAccumulatorsEmptyIterable() { ArrayList<TimestampedValue<Long>> emptyAccums = Lists.newArrayList(); assertEquals(TimestampedValue.atMinimumTimestamp(null), fn.mergeAccumulators(emptyAccums)); } @Test public void testMergeAccumulatorsDefaultAccumulator() { TimestampedValue<Long> defaultAccum = fn.createAccumulator(); assertEquals(TV, fn.mergeAccumulators(Lists.newArrayList(TV, defaultAccum))); } @Test public void testMergeAccumulatorsAllDefaultAccumulators() { TimestampedValue<Long> defaultAccum = fn.createAccumulator(); assertEquals(defaultAccum, fn.mergeAccumulators( Lists.newArrayList(defaultAccum, defaultAccum))); } @Test public void testMergeAccumulatorsNullIterable() { thrown.expect(NullPointerException.class); thrown.expectMessage("accumulators"); fn.mergeAccumulators(null); } @Test public void testExtractOutput() { assertEquals(TV.getValue(), fn.extractOutput(TV)); } @Test public void testExtractOutputDefaultAccumulator() { TimestampedValue<Long> accum = fn.createAccumulator(); assertThat(fn.extractOutput(accum), nullValue()); } @Test public void testExtractOutputNullValue() { TimestampedValue<Long> accum = TimestampedValue.of(null, baseTimestamp); assertEquals(null, fn.extractOutput(accum)); } @Test public void testDefaultCoderHandlesNull() throws CannotProvideCoderException { Latest.LatestFn<Long> fn = new Latest.LatestFn<>(); CoderRegistry registry = CoderRegistry.createDefault(); TimestampedValue.TimestampedValueCoder<Long> inputCoder = TimestampedValue.TimestampedValueCoder.of(VarLongCoder.of()); assertThat("Default output coder should handle null values", fn.getDefaultOutputCoder(registry, inputCoder), instanceOf(NullableCoder.class)); assertThat("Default accumulator coder should handle null values", fn.getAccumulatorCoder(registry, inputCoder), instanceOf(NullableCoder.class)); } }