>() {
121 | @Nullable
122 | @Override
123 | protected FilterRecord extends F> makeNext() {
124 | try {
125 | final FilterRecord extends F> record = filterStream.nextFilterRecord();
126 | if (record == null) {
127 | allDone();
128 | }
129 | return record;
130 | } catch (IOException ex) {
131 | throw new InvalidFilterException("read filter from file:" + persistentFilePath() + " failed", ex);
132 | }
133 | }
134 | };
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/filter-service-core/src/main/java/cn/leancloud/filter/service/PersistentStorageException.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 | public class PersistentStorageException extends RuntimeException {
4 | private static final long serialVersionUID = -1L;
5 |
6 | public PersistentStorageException() {
7 | }
8 |
9 | public PersistentStorageException(String message) {
10 | super(message);
11 | }
12 |
13 | public PersistentStorageException(String message, Throwable cause) {
14 | super(message, cause);
15 | }
16 |
17 | public PersistentStorageException(Throwable cause) {
18 | super(cause);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/filter-service-core/src/main/java/cn/leancloud/filter/service/Purgatory.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 | /**
4 | * A purgatory which have something to purge.
5 | */
6 | public interface Purgatory {
7 | void purge();
8 | }
9 |
--------------------------------------------------------------------------------
/filter-service-core/src/main/java/cn/leancloud/filter/service/PurgeFiltersJob.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 |
6 | public class PurgeFiltersJob implements Runnable {
7 | private static final Logger logger = LoggerFactory.getLogger(PurgeFiltersJob.class);
8 | private final Purgatory purgatory;
9 |
10 | PurgeFiltersJob(Purgatory purgatory) {
11 | this.purgatory = purgatory;
12 | }
13 |
14 | @Override
15 | public void run() {
16 | try {
17 | purgatory.purge();
18 | } catch (Exception ex) {
19 | logger.error("Purge bloom filter service failed.", ex);
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/filter-service-core/src/main/java/cn/leancloud/filter/service/ServerOptions.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 | import picocli.CommandLine.Command;
4 | import picocli.CommandLine.Option;
5 |
6 | import javax.annotation.Nullable;
7 |
8 | @Command(name = "filter-service",
9 | sortOptions = false,
10 | showDefaultValues = true,
11 | version = "filter-service v1.14",
12 | description = "filter-service is a daemon network service which is used to expose bloom filters " +
13 | "and operations by RESTFul API.",
14 | mixinStandardHelpOptions = true)
15 | final class ServerOptions {
16 | @Option(names = {"-c", "--configuration-file"},
17 | description = "The path to a YAML configuration file.")
18 | @Nullable
19 | private String configFilePath;
20 | @Option(names = {"-p", "--port"},
21 | defaultValue = "8080",
22 | description = "The http/https port on which filter-service is running.")
23 | private int port;
24 |
25 | @Option(names = {"-d", "--enable-doc-service"},
26 | defaultValue = "false",
27 | description = "true when you want to serve the testing document service under path \"/docs\".")
28 | private boolean docService;
29 |
30 | int port() {
31 | return port;
32 | }
33 |
34 | boolean docServiceEnabled() {
35 | return docService;
36 | }
37 |
38 | @Nullable
39 | String configFilePath() {
40 | return configFilePath;
41 | }
42 |
43 | ServerOptions() {
44 |
45 | }
46 |
47 | ServerOptions(@Nullable String configFilePath, int port, boolean docService) {
48 | this.configFilePath = configFilePath;
49 | this.port = port;
50 | this.docService = docService;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/filter-service-core/src/main/java/cn/leancloud/filter/service/ServiceParameterPreconditions.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 | import javax.annotation.Nullable;
4 |
5 | import static com.google.common.base.Strings.lenientFormat;
6 |
7 | public final class ServiceParameterPreconditions {
8 | /**
9 | * Ensures the truth of an expression involving one or more parameters to the calling method.
10 | *
11 | * @param paramName parameter name
12 | * @param expression a boolean expression
13 | * @throws IllegalArgumentException if {@code expression} is false
14 | */
15 | public static void checkParameter(String paramName, boolean expression) {
16 | if (!expression) {
17 | throw BadParameterException.invalidParameter(paramName);
18 | }
19 | }
20 |
21 | /**
22 | * Ensures the truth of an expression involving one or more parameters to the calling method.
23 | *
24 | * @param paramName parameter name
25 | * @param expression a boolean expression
26 | * @param errorMessage the exception message to use if the check fails; will be converted to a
27 | * string using {@link String#valueOf(Object)}
28 | * @throws IllegalArgumentException if {@code expression} is false
29 | */
30 | public static void checkParameter(String paramName, boolean expression, @Nullable Object errorMessage) {
31 | if (!expression) {
32 | throw BadParameterException.invalidParameter(paramName, String.valueOf(errorMessage));
33 | }
34 | }
35 |
36 | /**
37 | * Ensures the truth of an expression involving one or more parameters to the calling method.
38 | *
39 | * @param paramName parameter name
40 | * @param expression a boolean expression
41 | * @param errorMessageTemplate a template for the exception message should the check fail. The
42 | * message is formed by replacing each {@code %s} placeholder in the template with an
43 | * argument. These are matched by position - the first {@code %s} gets {@code
44 | * errorMessageArgs[0]}, etc. Unmatched arguments will be appended to the formatted message in
45 | * square braces. Unmatched placeholders will be left as-is.
46 | * @param errorMessageArgs the arguments to be substituted into the message template. Arguments
47 | * are converted to strings using {@link String#valueOf(Object)}.
48 | * @throws IllegalArgumentException if {@code expression} is false
49 | */
50 | public static void checkParameter(
51 | String paramName,
52 | boolean expression,
53 | @Nullable String errorMessageTemplate,
54 | @Nullable Object... errorMessageArgs) {
55 | if (!expression) {
56 | throw BadParameterException.invalidParameter(paramName, lenientFormat(errorMessageTemplate, errorMessageArgs));
57 | }
58 | }
59 |
60 | /**
61 | * Ensures the truth of an expression involving one or more parameters to the calling method.
62 | *
63 | * See {@link #checkParameter(String, boolean, String, Object...)} for details.
64 | */
65 | public static void checkParameter(String paramName, boolean b, @Nullable String errorMessageTemplate, int p1) {
66 | if (!b) {
67 | throw BadParameterException.invalidParameter(paramName, lenientFormat(errorMessageTemplate, p1));
68 | }
69 | }
70 |
71 | /**
72 | * Ensures the truth of an expression involving one or more parameters to the calling method.
73 | *
74 | *
See {@link #checkParameter(String, boolean, String, Object...)} for details.
75 | */
76 | public static void checkParameter(
77 | String paramName,
78 | boolean b, @Nullable String errorMessageTemplate, @Nullable Object p1) {
79 | if (!b) {
80 | throw BadParameterException.invalidParameter(paramName, lenientFormat(errorMessageTemplate, p1));
81 | }
82 | }
83 |
84 | public static T checkNotNull(String paramName, @Nullable T obj) {
85 | if (obj == null) {
86 | throw BadParameterException.requiredParameter(paramName);
87 | }
88 |
89 | return obj;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/filter-service-core/src/main/java/cn/leancloud/filter/service/UnfinishedFilterException.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 | public class UnfinishedFilterException extends InvalidFilterException {
4 | private static final long serialVersionUID = -1L;
5 |
6 | public UnfinishedFilterException() {
7 | }
8 |
9 | public UnfinishedFilterException(String message) {
10 | super(message);
11 | }
12 |
13 | public UnfinishedFilterException(String message, Throwable cause) {
14 | super(message, cause);
15 | }
16 |
17 | public UnfinishedFilterException(Throwable cause) {
18 | super(cause);
19 | }
20 |
21 | public static UnfinishedFilterException shortReadFilterHeader(String location, int shortBytes) {
22 | return new UnfinishedFilterException("not enough bytes to read filter record header from: " + location + ". " +
23 | shortBytes + " bytes short.");
24 | }
25 |
26 | public static UnfinishedFilterException shortReadFilterBody(String location, int shortBytes) {
27 | return new UnfinishedFilterException("not enough bytes to read filter record body from: " + location + ". " +
28 | shortBytes + " bytes short.");
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/filter-service-core/src/main/java/cn/leancloud/filter/service/package-info.java:
--------------------------------------------------------------------------------
1 |
2 |
3 | @NonNullByDefault
4 | package cn.leancloud.filter.service;
--------------------------------------------------------------------------------
/filter-service-core/src/main/java/cn/leancloud/filter/service/utils/AbstractIterator.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service.utils;
2 |
3 | import javax.annotation.Nullable;
4 | import java.util.Iterator;
5 | import java.util.NoSuchElementException;
6 |
7 | /**
8 | * A base class that simplifies implementing an iterator
9 | * @param The type of thing we are iterating over
10 | */
11 | public abstract class AbstractIterator implements Iterator {
12 | private enum State {
13 | READY, NOT_READY, DONE, FAILED
14 | }
15 |
16 | private State state = State.NOT_READY;
17 | @Nullable
18 | private T next;
19 |
20 | @Override
21 | public boolean hasNext() {
22 | switch (state) {
23 | case FAILED:
24 | throw new IllegalStateException("iterator is in failed state");
25 | case DONE:
26 | return false;
27 | case READY:
28 | return true;
29 | default:
30 | return maybeComputeNext();
31 | }
32 | }
33 |
34 | @Override
35 | public T next() {
36 | if (!hasNext())
37 | throw new NoSuchElementException();
38 | state = State.NOT_READY;
39 | if (next == null)
40 | throw new IllegalStateException("expected item but none found");
41 | return next;
42 | }
43 |
44 | @Override
45 | public void remove() {
46 | throw new UnsupportedOperationException("removal not supported");
47 | }
48 |
49 | public T peek() {
50 | if (!hasNext())
51 | throw new NoSuchElementException();
52 | assert next != null;
53 | return next;
54 | }
55 |
56 | @Nullable
57 | protected T allDone() {
58 | state = State.DONE;
59 | return null;
60 | }
61 |
62 | @Nullable
63 | protected abstract T makeNext();
64 |
65 | private Boolean maybeComputeNext() {
66 | state = State.FAILED;
67 | next = makeNext();
68 | if (state == State.DONE) {
69 | return false;
70 | } else {
71 | state = State.READY;
72 | return true;
73 | }
74 | }
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/filter-service-core/src/main/java/cn/leancloud/filter/service/utils/ChecksumedBufferedOutputStream.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service.utils;
2 |
3 | import java.io.FilterOutputStream;
4 | import java.io.IOException;
5 | import java.io.OutputStream;
6 | import java.util.zip.Checksum;
7 |
8 | public final class ChecksumedBufferedOutputStream extends FilterOutputStream {
9 | /**
10 | * The internal buffer where data is stored.
11 | */
12 | private final byte[] buf;
13 |
14 | private final Checksum crc;
15 |
16 | /**
17 | * The number of valid bytes in the buffer. This value is always
18 | * in the range {@code 0} through {@code buf.length}; elements
19 | * {@code buf[0]} through {@code buf[count-1]} contain valid
20 | * byte data.
21 | */
22 | private int count;
23 |
24 | /**
25 | * Creates a new buffered output stream to write data to the
26 | * specified underlying output stream.
27 | *
28 | * @param out the underlying output stream.
29 | */
30 | public ChecksumedBufferedOutputStream(OutputStream out) {
31 | this(out, 8192);
32 | }
33 |
34 | /**
35 | * Creates a new buffered output stream to write data to the
36 | * specified underlying output stream with the specified buffer
37 | * size.
38 | *
39 | * @param out the underlying output stream.
40 | * @param size the buffer size.
41 | * @exception IllegalArgumentException if size <= 0.
42 | */
43 | public ChecksumedBufferedOutputStream(OutputStream out, int size) {
44 | super(out);
45 | if (size <= 0) {
46 | throw new IllegalArgumentException("Buffer size <= 0");
47 | }
48 | buf = new byte[size];
49 | crc = Crc32C.create();
50 | }
51 |
52 | /** Flush the internal buffer */
53 | private void flushBuffer() throws IOException {
54 | if (count > 0) {
55 | out.write(buf, 0, count);
56 | crc.update(buf, 0, count);
57 | count = 0;
58 | }
59 | }
60 |
61 | /**
62 | * Writes the specified byte to this buffered output stream.
63 | *
64 | * @param b the byte to be written.
65 | * @exception IOException if an I/O error occurs.
66 | */
67 | @Override
68 | public synchronized void write(int b) throws IOException {
69 | if (count >= buf.length) {
70 | flushBuffer();
71 | }
72 | buf[count++] = (byte)b;
73 | }
74 |
75 | /**
76 | * Writes len
bytes from the specified byte array
77 | * starting at offset off
to this buffered output stream.
78 | *
79 | * Ordinarily this method stores bytes from the given array into this
80 | * stream's buffer, flushing the buffer to the underlying output stream as
81 | * needed. If the requested length is at least as large as this stream's
82 | * buffer, however, then this method will flush the buffer and write the
83 | * bytes directly to the underlying output stream. Thus redundant
84 | * BufferedOutputStream
s will not copy data unnecessarily.
85 | *
86 | * @param b the data.
87 | * @param off the start offset in the data.
88 | * @param len the number of bytes to write.
89 | * @exception IOException if an I/O error occurs.
90 | */
91 | @Override
92 | public synchronized void write(byte[] b, int off, int len) throws IOException {
93 | if (len >= buf.length) {
94 | /* If the request length exceeds the size of the output buffer,
95 | flush the output buffer and then write the data directly.
96 | In this way buffered streams will cascade harmlessly. */
97 | flushBuffer();
98 | out.write(b, off, len);
99 | crc.update(b, off, len);
100 | return;
101 | }
102 | if (len > buf.length - count) {
103 | flushBuffer();
104 | }
105 | System.arraycopy(b, off, buf, count, len);
106 | count += len;
107 | }
108 |
109 | /**
110 | * Flushes this buffered output stream. This forces any buffered
111 | * output bytes to be written out to the underlying output stream.
112 | *
113 | * @exception IOException if an I/O error occurs.
114 | * @see java.io.FilterOutputStream#out
115 | */
116 | @Override
117 | public synchronized void flush() throws IOException {
118 | flushBuffer();
119 | out.flush();
120 | }
121 |
122 | public synchronized long checksum() {
123 | return crc.getValue();
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/filter-service-core/src/main/java/cn/leancloud/filter/service/utils/Crc32C.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service.utils;
2 |
3 | import java.lang.invoke.MethodHandle;
4 | import java.lang.invoke.MethodHandles;
5 | import java.lang.invoke.MethodType;
6 | import java.nio.ByteBuffer;
7 | import java.util.zip.Checksum;
8 |
9 | /**
10 | * A class that can be used to compute the CRC32C (Castagnoli) of a ByteBuffer or array of bytes.
11 | *
12 | * We use java.util.zip.CRC32C (introduced in Java 9) if it is available and fallback to PureJavaCrc32C, otherwise.
13 | * java.util.zip.CRC32C is significantly faster on reasonably modern CPUs as it uses the CRC32 instruction introduced
14 | * in SSE4.2.
15 | *
16 | * NOTE: This class is copied from Kafka.
17 | */
18 | public final class Crc32C {
19 |
20 | private static final ChecksumFactory CHECKSUM_FACTORY;
21 |
22 | static {
23 | if (Java.IS_JAVA9_COMPATIBLE)
24 | CHECKSUM_FACTORY = new Java9ChecksumFactory();
25 | else
26 | CHECKSUM_FACTORY = new PureJavaChecksumFactory();
27 | }
28 |
29 | private Crc32C() {}
30 |
31 | /**
32 | * Compute the CRC32C (Castagnoli) of the segment of the byte array given by the specified size and offset
33 | *
34 | * @param bytes The bytes to checksum
35 | * @param offset the offset at which to begin the checksum computation
36 | * @param size the number of bytes to checksum
37 | * @return The CRC32C
38 | */
39 | public static long compute(byte[] bytes, int offset, int size) {
40 | Checksum crc = create();
41 | crc.update(bytes, offset, size);
42 | return crc.getValue();
43 | }
44 |
45 | /**
46 | * Compute the CRC32C (Castagnoli) of a byte buffer from a given offset (relative to the buffer's current position)
47 | *
48 | * @param buffer The buffer with the underlying data
49 | * @param offset The offset relative to the current position
50 | * @param size The number of bytes beginning from the offset to include
51 | * @return The CRC32C
52 | */
53 | public static long compute(ByteBuffer buffer, int offset, int size) {
54 | Checksum crc = create();
55 | updateChecksums(crc, buffer, offset, size);
56 | return crc.getValue();
57 | }
58 |
59 | public static Checksum create() {
60 | return CHECKSUM_FACTORY.create();
61 | }
62 |
63 | private static void updateChecksums(Checksum checksum, ByteBuffer buffer, int offset, int length) {
64 | if (buffer.hasArray()) {
65 | checksum.update(buffer.array(), buffer.position() + buffer.arrayOffset() + offset, length);
66 | } else {
67 | int start = buffer.position() + offset;
68 | for (int i = start; i < start + length; i++)
69 | checksum.update(buffer.get(i));
70 | }
71 | }
72 |
73 | private interface ChecksumFactory {
74 | Checksum create();
75 | }
76 |
77 | private static class Java9ChecksumFactory implements ChecksumFactory {
78 | private static final MethodHandle CONSTRUCTOR;
79 |
80 | static {
81 | try {
82 | Class> cls = Class.forName("java.util.zip.CRC32C");
83 | CONSTRUCTOR = MethodHandles.publicLookup().findConstructor(cls, MethodType.methodType(void.class));
84 | } catch (ReflectiveOperationException e) {
85 | // Should never happen
86 | throw new RuntimeException(e);
87 | }
88 | }
89 |
90 | @Override
91 | public Checksum create() {
92 | try {
93 | return (Checksum) CONSTRUCTOR.invoke();
94 | } catch (Throwable throwable) {
95 | // Should never happen
96 | throw new RuntimeException(throwable);
97 | }
98 | }
99 | }
100 |
101 | private static class PureJavaChecksumFactory implements ChecksumFactory {
102 | @Override
103 | public Checksum create() {
104 | return new PureJavaCrc32C();
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/filter-service-core/src/main/java/cn/leancloud/filter/service/utils/DefaultTimer.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service.utils;
2 |
3 | import java.time.ZoneOffset;
4 | import java.time.ZonedDateTime;
5 |
6 | public final class DefaultTimer implements Timer{
7 | @Override
8 | public ZonedDateTime utcNow() {
9 | return ZonedDateTime.now(ZoneOffset.UTC);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/filter-service-core/src/main/java/cn/leancloud/filter/service/utils/Java.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service.utils;
2 |
3 | import java.util.StringTokenizer;
4 |
5 | public final class Java {
6 |
7 | private Java() { }
8 |
9 | private static final Version VERSION = parseVersion(System.getProperty("java.specification.version"));
10 |
11 | // Package private for testing
12 | static Version parseVersion(String versionString) {
13 | final StringTokenizer st = new StringTokenizer(versionString, ".");
14 | int majorVersion = Integer.parseInt(st.nextToken());
15 | int minorVersion;
16 | if (st.hasMoreTokens())
17 | minorVersion = Integer.parseInt(st.nextToken());
18 | else
19 | minorVersion = 0;
20 | return new Version(majorVersion, minorVersion);
21 | }
22 |
23 | // Having these as static final provides the best opportunity for compiler optimization
24 | public static final boolean IS_JAVA9_COMPATIBLE = VERSION.isJava9Compatible();
25 |
26 | // Package private for testing
27 | static class Version {
28 | public final int majorVersion;
29 | public final int minorVersion;
30 |
31 | private Version(int majorVersion, int minorVersion) {
32 | this.majorVersion = majorVersion;
33 | this.minorVersion = minorVersion;
34 | }
35 |
36 | @Override
37 | public String toString() {
38 | return "Version(majorVersion=" + majorVersion +
39 | ", minorVersion=" + minorVersion + ")";
40 | }
41 |
42 | // Package private for testing
43 | boolean isJava9Compatible() {
44 | return majorVersion >= 9;
45 | }
46 |
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/filter-service-core/src/main/java/cn/leancloud/filter/service/utils/Timer.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service.utils;
2 |
3 | import java.time.ZonedDateTime;
4 |
5 | public interface Timer {
6 | Timer DEFAULT_TIMER = new DefaultTimer();
7 |
8 | ZonedDateTime utcNow();
9 | }
10 |
--------------------------------------------------------------------------------
/filter-service-core/src/main/java/cn/leancloud/filter/service/utils/package-info.java:
--------------------------------------------------------------------------------
1 |
2 |
3 | @NonNullByDefault
4 | package cn.leancloud.filter.service.utils;
5 |
6 | import cn.leancloud.filter.service.NonNullByDefault;
--------------------------------------------------------------------------------
/filter-service-core/src/main/resources/log4j2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/filter-service-core/src/test/java/cn/leancloud/filter/service/AbstractBloomFilterConfigTest.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 | import org.junit.Test;
4 |
5 | import java.util.concurrent.ThreadLocalRandom;
6 |
7 | import static org.assertj.core.api.Assertions.assertThat;
8 | import static org.assertj.core.api.Assertions.assertThatThrownBy;
9 |
10 | public class AbstractBloomFilterConfigTest {
11 | @Test
12 | public void testGetAndSetExpectedInsertions() {
13 | final int expectedEInsertions = ThreadLocalRandom.current().nextInt(1, Integer.MAX_VALUE);
14 | final TestingBloomFilterConfig config = new TestingBloomFilterConfig();
15 | assertThat(config.expectedInsertions()).isEqualTo(Configuration.defaultExpectedInsertions());
16 | assertThat(config.setExpectedInsertions(expectedEInsertions)).isSameAs(config);
17 | assertThat(config.expectedInsertions()).isEqualTo(expectedEInsertions);
18 | }
19 |
20 | @Test
21 | public void testGetAndSetFpp() {
22 | final double expectedFpp = ThreadLocalRandom.current().nextDouble(0.0001, 1);
23 | final TestingBloomFilterConfig config = new TestingBloomFilterConfig();
24 | assertThat(config.fpp()).isEqualTo(Configuration.defaultFalsePositiveProbability());
25 | assertThat(config.setFpp(expectedFpp)).isSameAs(config);
26 | assertThat(config.fpp()).isEqualTo(expectedFpp);
27 | }
28 |
29 | @Test
30 | public void testGetAndSetInvalidExpectedInsertions() {
31 | final int invalidExpectedInsertions = -1 * Math.abs(ThreadLocalRandom.current().nextInt());
32 | final TestingBloomFilterConfig config = new TestingBloomFilterConfig();
33 |
34 | assertThatThrownBy(() -> config.setExpectedInsertions(invalidExpectedInsertions))
35 | .isInstanceOf(BadParameterException.class)
36 | .hasMessageContaining("invalid parameter");
37 | }
38 |
39 | @Test
40 | public void testGetAndSetInvalidFpp() {
41 | final double invalidFpp = ThreadLocalRandom.current().nextDouble(1, Long.MAX_VALUE);
42 | final TestingBloomFilterConfig config = new TestingBloomFilterConfig();
43 |
44 | assertThatThrownBy(() -> config.setFpp(invalidFpp))
45 | .isInstanceOf(BadParameterException.class)
46 | .hasMessageContaining("invalid parameter");
47 | }
48 |
49 | @Test
50 | public void testEquals() {
51 | final double fpp = ThreadLocalRandom.current().nextDouble(0.0001, 1);
52 | final int expectedInsertions = ThreadLocalRandom.current().nextInt(1, Integer.MAX_VALUE);
53 | final TestingBloomFilterConfig filterA = new TestingBloomFilterConfig()
54 | .setFpp(fpp)
55 | .setExpectedInsertions(expectedInsertions);
56 |
57 | final TestingBloomFilterConfig filterB = new TestingBloomFilterConfig()
58 | .setFpp(fpp)
59 | .setExpectedInsertions(expectedInsertions);
60 |
61 | assertThat(filterA).isEqualTo(filterB);
62 | }
63 |
64 | @Test
65 | public void testHashCode() {
66 | final double fpp = ThreadLocalRandom.current().nextDouble(0.0001, 1);
67 | final int expectedInsertions = ThreadLocalRandom.current().nextInt(1, Integer.MAX_VALUE);
68 | final TestingBloomFilterConfig filterA = new TestingBloomFilterConfig()
69 | .setFpp(fpp)
70 | .setExpectedInsertions(expectedInsertions);
71 |
72 | final TestingBloomFilterConfig filterB = new TestingBloomFilterConfig()
73 | .setFpp(fpp)
74 | .setExpectedInsertions(expectedInsertions);
75 |
76 | assertThat(filterA.hashCode()).isEqualTo(filterB.hashCode());
77 | }
78 |
79 | private static class TestingBloomFilterConfig extends AbstractBloomFilterConfig {
80 | @Override
81 | protected TestingBloomFilterConfig self() {
82 | return this;
83 | }
84 | }
85 | }
--------------------------------------------------------------------------------
/filter-service-core/src/test/java/cn/leancloud/filter/service/AdjustableTimer.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 | import cn.leancloud.filter.service.utils.Timer;
4 |
5 | import java.time.ZoneOffset;
6 | import java.time.ZonedDateTime;
7 |
8 | public class AdjustableTimer implements Timer {
9 | private ZonedDateTime now;
10 |
11 | public AdjustableTimer() {
12 | this.now = ZonedDateTime.now(ZoneOffset.UTC);
13 | }
14 |
15 | public void setNow(ZonedDateTime now) {
16 | this.now = now;
17 | }
18 |
19 | @Override
20 | public ZonedDateTime utcNow() {
21 | return now;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/filter-service-core/src/test/java/cn/leancloud/filter/service/BackgroundJobSchedulerTest.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 | import com.linecorp.armeria.common.metric.NoopMeterRegistry;
4 | import io.micrometer.core.instrument.MeterRegistry;
5 | import org.junit.After;
6 | import org.junit.Before;
7 | import org.junit.Test;
8 |
9 | import java.time.Duration;
10 | import java.util.concurrent.CountDownLatch;
11 | import java.util.concurrent.Executors;
12 | import java.util.concurrent.ScheduledExecutorService;
13 | import java.util.concurrent.atomic.AtomicBoolean;
14 | import java.util.concurrent.atomic.AtomicInteger;
15 |
16 | import static org.assertj.core.api.Assertions.assertThat;
17 |
18 | public class BackgroundJobSchedulerTest {
19 | private static final String jobName = "TestingJobName";
20 | private ScheduledExecutorService scheduledExecutorService;
21 | private MeterRegistry registry;
22 | private BackgroundJobScheduler scheduler;
23 |
24 | @Before
25 | public void setUp() throws Exception {
26 | scheduledExecutorService = Executors.newScheduledThreadPool(1);
27 | registry = NoopMeterRegistry.get();
28 | scheduler = new BackgroundJobScheduler(registry, scheduledExecutorService);
29 | }
30 |
31 | @After
32 | public void tearDown() throws Exception {
33 | scheduler.stop();
34 | scheduledExecutorService.shutdown();
35 | }
36 |
37 | @Test
38 | public void testStopScheduler() throws Exception {
39 | final CountDownLatch executed = new CountDownLatch(1);
40 | final AtomicInteger executedTimes = new AtomicInteger();
41 | final AtomicBoolean interrupt = new AtomicBoolean(false);
42 | scheduler.scheduleFixedIntervalJob(() -> {
43 | try {
44 | executed.countDown();
45 | Thread.sleep(1000);
46 | executedTimes.incrementAndGet();
47 | } catch (InterruptedException ex) {
48 | interrupt.set(true);
49 | }
50 | }, jobName, Duration.ofMillis(10));
51 |
52 | executed.await();
53 | scheduler.stop();
54 | assertThat(executedTimes).hasValue(1);
55 | assertThat(interrupt).isFalse();
56 | }
57 | }
--------------------------------------------------------------------------------
/filter-service-core/src/test/java/cn/leancloud/filter/service/BootstrapTest.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 | import cn.leancloud.filter.service.Bootstrap.ParseCommandLineArgsResult;
4 | import org.apache.commons.io.FileUtils;
5 | import org.junit.Test;
6 | import picocli.CommandLine.ExitCode;
7 |
8 | import java.nio.file.Paths;
9 |
10 | import static org.assertj.core.api.Assertions.assertThat;
11 |
12 | public class BootstrapTest {
13 |
14 | @Test
15 | public void testStartStop() throws Exception {
16 | final String configFilePath = "src/test/resources/testing-configuration.yaml";
17 | final ServerOptions opts = new ServerOptions(configFilePath, 8080, false);
18 | final Bootstrap bootstrap = new Bootstrap(opts);
19 | bootstrap.start(true);
20 |
21 | bootstrap.stop();
22 | FileUtils.forceDelete(Paths.get("lock").toFile());
23 | }
24 |
25 | @Test
26 | public void testHelp() {
27 | final String[] args = new String[]{"-h"};
28 | ParseCommandLineArgsResult ret = Bootstrap.parseCommandLineArgs(args);
29 | assertThat(ret.isExit()).isTrue();
30 | assertThat(ret.getExitCode()).isEqualTo(ExitCode.OK);
31 | assertThat(ret.getOptions()).isNull();
32 | }
33 |
34 | @Test
35 | public void testHelp2() {
36 | final String[] args = new String[]{"--help"};
37 | ParseCommandLineArgsResult ret = Bootstrap.parseCommandLineArgs(args);
38 | assertThat(ret.isExit()).isTrue();
39 | assertThat(ret.getExitCode()).isEqualTo(ExitCode.OK);
40 | assertThat(ret.getOptions()).isNull();
41 | }
42 |
43 | @Test
44 | public void testVersion() {
45 | final String[] args = new String[]{"-V"};
46 | ParseCommandLineArgsResult ret = Bootstrap.parseCommandLineArgs(args);
47 | assertThat(ret.isExit()).isTrue();
48 | assertThat(ret.getExitCode()).isEqualTo(ExitCode.OK);
49 | assertThat(ret.getOptions()).isNull();
50 | }
51 |
52 | @Test
53 | public void testVersion2() {
54 | final String[] args = new String[]{"--version"};
55 | ParseCommandLineArgsResult ret = Bootstrap.parseCommandLineArgs(args);
56 | assertThat(ret.isExit()).isTrue();
57 | assertThat(ret.getExitCode()).isEqualTo(ExitCode.OK);
58 | assertThat(ret.getOptions()).isNull();
59 | }
60 |
61 | @Test
62 | public void testUnknownArgument() {
63 | final String[] args = new String[]{"-unknown"};
64 | ParseCommandLineArgsResult ret = Bootstrap.parseCommandLineArgs(args);
65 | assertThat(ret.isExit()).isTrue();
66 | assertThat(ret.getExitCode()).isEqualTo(ExitCode.USAGE);
67 | assertThat(ret.getOptions()).isNull();
68 | }
69 |
70 | @Test
71 | public void testInvalidPort() {
72 | String[] args = new String[]{"-p", "wahaha"};
73 | ParseCommandLineArgsResult ret = Bootstrap.parseCommandLineArgs(args);
74 | assertThat(ret.isExit()).isTrue();
75 | assertThat(ret.getExitCode()).isEqualTo(ExitCode.USAGE);
76 | assertThat(ret.getOptions()).isNull();
77 | }
78 |
79 | @Test
80 | public void testInvalidPort2() {
81 | String[] args = new String[]{"--port", "wahaha"};
82 | ParseCommandLineArgsResult ret = Bootstrap.parseCommandLineArgs(args);
83 | assertThat(ret.isExit()).isTrue();
84 | assertThat(ret.getExitCode()).isEqualTo(ExitCode.USAGE);
85 | assertThat(ret.getOptions()).isNull();
86 | }
87 |
88 | @Test
89 | public void testInvalidEnableDocService() {
90 | String[] args = new String[]{"-d", "wahaha"};
91 | ParseCommandLineArgsResult ret = Bootstrap.parseCommandLineArgs(args);
92 | assertThat(ret.isExit()).isTrue();
93 | assertThat(ret.getExitCode()).isEqualTo(ExitCode.USAGE);
94 | assertThat(ret.getOptions()).isNull();
95 | }
96 |
97 | @Test
98 | public void testArgsInAbbreviationForm() {
99 | String[] args = new String[]{"-d", "-c", "path/to/config", "-p", "8080"};
100 | ParseCommandLineArgsResult ret = Bootstrap.parseCommandLineArgs(args);
101 | assertThat(ret.isExit()).isFalse();
102 |
103 | assertThat(ret.getExitCode()).isEqualTo(ExitCode.OK);
104 | ServerOptions options = ret.getOptions();
105 | assertThat(options).isNotNull();
106 | assertThat(options.docServiceEnabled()).isTrue();
107 | assertThat(options.port()).isEqualTo(8080);
108 | assertThat(options.configFilePath()).isEqualTo("path/to/config");
109 | }
110 |
111 | @Test
112 | public void testArgsInFullForm() {
113 | String[] args = new String[]{"--enable-doc-service", "--configuration-file", "path/to/config", "--port", "8080"};
114 | ParseCommandLineArgsResult ret = Bootstrap.parseCommandLineArgs(args);
115 | assertThat(ret.isExit()).isFalse();
116 |
117 | assertThat(ret.getExitCode()).isEqualTo(ExitCode.OK);
118 | ServerOptions options = ret.getOptions();
119 | assertThat(options).isNotNull();
120 | assertThat(options.docServiceEnabled()).isTrue();
121 | assertThat(options.port()).isEqualTo(8080);
122 | assertThat(options.configFilePath()).isEqualTo("path/to/config");
123 | }
124 | }
--------------------------------------------------------------------------------
/filter-service-core/src/test/java/cn/leancloud/filter/service/CountUpdateBloomFilterFactoryTest.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 | import org.junit.Before;
4 | import org.junit.Test;
5 |
6 | import java.io.ByteArrayInputStream;
7 | import java.io.IOException;
8 | import java.io.InputStream;
9 | import java.util.concurrent.atomic.LongAdder;
10 |
11 | import static org.assertj.core.api.Assertions.assertThat;
12 | import static org.assertj.core.api.Assertions.assertThatThrownBy;
13 | import static org.mockito.Mockito.*;
14 |
15 | @SuppressWarnings("unchecked")
16 | public class CountUpdateBloomFilterFactoryTest {
17 | private LongAdder filterUpdateTimesCounter;
18 | private BloomFilterFactory innerFilterFactory;
19 | private CountUpdateBloomFilterFactory wrapper;
20 |
21 | @Before
22 | public void setUp() {
23 | filterUpdateTimesCounter = new LongAdder();
24 | innerFilterFactory = mock(BloomFilterFactory.class);
25 | wrapper = new CountUpdateBloomFilterFactory(innerFilterFactory, filterUpdateTimesCounter);
26 | }
27 |
28 | @Test
29 | public void testDelegate() throws Exception {
30 | final InputStream in = new ByteArrayInputStream(new byte[0]);
31 | final ExpirableBloomFilterConfig config = new ExpirableBloomFilterConfig();
32 | final BloomFilter filter = mock(BloomFilter.class);
33 | when(innerFilterFactory.createFilter(config)).thenReturn(filter);
34 | when(innerFilterFactory.readFrom(in)).thenReturn(filter);
35 |
36 | assertThat(wrapper.createFilter(config)).isEqualTo(new CountUpdateBloomFilterWrapper(filter, filterUpdateTimesCounter));
37 | assertThat(wrapper.readFrom(in)).isEqualTo(new CountUpdateBloomFilterWrapper(filter, filterUpdateTimesCounter));
38 | assertThat(filterUpdateTimesCounter.sum()).isEqualTo(1);
39 |
40 | verify(innerFilterFactory, times(1)).createFilter(config);
41 | verify(innerFilterFactory, times(1)).readFrom(in);
42 | }
43 |
44 | @Test
45 | public void testReadFromThrowsException() throws Exception {
46 | final InputStream in = new ByteArrayInputStream(new byte[0]);
47 | final IOException expectedEx = new IOException("expected exception");
48 |
49 | doThrow(expectedEx).when(innerFilterFactory).readFrom(in);
50 |
51 | assertThatThrownBy(() -> wrapper.readFrom(in)).isEqualTo(expectedEx);
52 | verify(innerFilterFactory, times(1)).readFrom(in);
53 | }
54 | }
--------------------------------------------------------------------------------
/filter-service-core/src/test/java/cn/leancloud/filter/service/CountUpdateBloomFilterWrapperTest.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import org.junit.Before;
5 | import org.junit.Test;
6 |
7 | import java.io.ByteArrayOutputStream;
8 | import java.io.IOException;
9 | import java.io.OutputStream;
10 | import java.time.Duration;
11 | import java.util.concurrent.atomic.LongAdder;
12 |
13 | import static org.assertj.core.api.Assertions.assertThat;
14 | import static org.assertj.core.api.Assertions.assertThatThrownBy;
15 | import static org.mockito.Mockito.*;
16 |
17 | public class CountUpdateBloomFilterWrapperTest {
18 | private LongAdder filterUpdateTimesCounter;
19 | private BloomFilter innerFilter;
20 | private CountUpdateBloomFilterWrapper wrapper;
21 |
22 | @Before
23 | public void setUp() {
24 | filterUpdateTimesCounter = new LongAdder();
25 | innerFilter = mock(BloomFilter.class);
26 | wrapper = new CountUpdateBloomFilterWrapper(innerFilter, filterUpdateTimesCounter);
27 | }
28 |
29 | @Test
30 | public void testDelegate() throws IOException {
31 | final int expectedInsertions = 1000;
32 | final double fpp = 0.01;
33 | final String testingValue = "testingValue";
34 | final OutputStream out = new ByteArrayOutputStream();
35 |
36 | when(innerFilter.expectedInsertions()).thenReturn(expectedInsertions);
37 | when(innerFilter.fpp()).thenReturn(fpp);
38 | when(innerFilter.mightContain(testingValue)).thenReturn(true);
39 | when(innerFilter.set(testingValue)).thenReturn(false);
40 | when(innerFilter.valid()).thenReturn(false);
41 | doNothing().when(innerFilter).writeTo(out);
42 |
43 | assertThat(wrapper.expectedInsertions()).isEqualTo(expectedInsertions);
44 | assertThat(wrapper.fpp()).isEqualTo(fpp);
45 | assertThat(wrapper.mightContain(testingValue)).isTrue();
46 | assertThat(wrapper.set(testingValue)).isFalse();
47 | assertThat(wrapper.valid()).isFalse();
48 | wrapper.writeTo(out);
49 | assertThat(filterUpdateTimesCounter.sum()).isEqualTo(1);
50 |
51 | verify(innerFilter, times(1)).expectedInsertions();
52 | verify(innerFilter, times(1)).fpp();
53 | verify(innerFilter, times(1)).mightContain(testingValue);
54 | verify(innerFilter, times(1)).set(testingValue);
55 | verify(innerFilter, times(1)).valid();
56 | verify(innerFilter, times(1)).writeTo(out);
57 | }
58 |
59 | @Test
60 | public void testUpdateCounter() {
61 | final String testingValue = "testingValue";
62 | when(innerFilter.set(testingValue)).thenReturn(false);
63 | assertThat(filterUpdateTimesCounter.sum()).isZero();
64 | assertThat(wrapper.set(testingValue)).isFalse();
65 | assertThat(filterUpdateTimesCounter.sum()).isEqualTo(1);
66 | }
67 |
68 | @Test
69 | public void testWriteToThrowException() throws IOException {
70 | final OutputStream out = new ByteArrayOutputStream();
71 | final IOException expectedException = new IOException("expected IO exception");
72 | doThrow(expectedException).when(innerFilter).writeTo(out);
73 |
74 | assertThatThrownBy(() -> wrapper.writeTo(out)).isSameAs(expectedException);
75 | verify(innerFilter, times(1)).writeTo(out);
76 | }
77 |
78 | @Test
79 | public void testToJson() {
80 | final Duration validPeriodAfterAccess = Duration.ofSeconds(10);
81 | final ExpirableBloomFilterConfig config = new ExpirableBloomFilterConfig();
82 | config.setValidPeriodAfterAccess(validPeriodAfterAccess);
83 | final GuavaBloomFilter innerFilter = new GuavaBloomFilterFactory().createFilter(config);
84 | final ObjectMapper mapper = new ObjectMapper();
85 | final String expectedJson = mapper.valueToTree(innerFilter).toString();
86 |
87 | final CountUpdateBloomFilterWrapper filter = new CountUpdateBloomFilterWrapper(innerFilter, filterUpdateTimesCounter);
88 | assertThat(mapper.valueToTree(filter).toString()).isEqualTo(expectedJson);
89 | }
90 | }
--------------------------------------------------------------------------------
/filter-service-core/src/test/java/cn/leancloud/filter/service/ErrorsTest.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.assertj.core.api.Assertions.assertThat;
6 |
7 | public class ErrorsTest {
8 | @Test
9 | public void testUnknownError() {
10 | assertThat(Errors.UNKNOWN_ERROR.buildErrorInfoInJson().toString())
11 | .isEqualTo("{\"error\":\"unknown error\",\"code\":-1}");
12 |
13 | assertThat(Errors.UNKNOWN_ERROR.buildErrorInfoInJson("error msg").toString())
14 | .isEqualTo("{\"error\":\"error msg\",\"code\":-1}");
15 | }
16 |
17 | @Test
18 | public void testBadParameter() {
19 | assertThat(Errors.BAD_PARAMETER.buildErrorInfoInJson().toString())
20 | .isEqualTo("{\"error\":\"invalid parameter\",\"code\":1}");
21 |
22 | assertThat(Errors.BAD_PARAMETER.buildErrorInfoInJson("error msg").toString())
23 | .isEqualTo("{\"error\":\"error msg\",\"code\":1}");
24 | }
25 |
26 | @Test
27 | public void testFilterNotFound() {
28 | assertThat(Errors.FILTER_NOT_FOUND.buildErrorInfoInJson().toString())
29 | .isEqualTo("{\"error\":\"filter not found\",\"code\":2}");
30 |
31 | assertThat(Errors.FILTER_NOT_FOUND.buildErrorInfoInJson("error msg").toString())
32 | .isEqualTo("{\"error\":\"error msg\",\"code\":2}");
33 | }
34 | }
--------------------------------------------------------------------------------
/filter-service-core/src/test/java/cn/leancloud/filter/service/ExpirableBloomFilterConfigTest.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 | import org.junit.Test;
4 |
5 | import java.time.Duration;
6 | import java.util.concurrent.ThreadLocalRandom;
7 |
8 | import static org.assertj.core.api.Assertions.assertThat;
9 | import static org.assertj.core.api.Assertions.assertThatThrownBy;
10 |
11 |
12 | public class ExpirableBloomFilterConfigTest {
13 | @Test
14 | public void testGetAndSetValidPeriodAfterCreate() {
15 | final int expectedValidPeriod = ThreadLocalRandom.current().nextInt(0, Integer.MAX_VALUE);
16 | final ExpirableBloomFilterConfig config = new ExpirableBloomFilterConfig();
17 | assertThat(config.validPeriodAfterCreate()).isEqualTo(Configuration.defaultValidPeriodAfterCreate());
18 | assertThat(config.setValidPeriodAfterCreate(Duration.ofSeconds(expectedValidPeriod))).isSameAs(config);
19 | assertThat(config.validPeriodAfterCreate()).isEqualTo(Duration.ofSeconds(expectedValidPeriod));
20 | }
21 |
22 | @Test
23 | public void testSetNegativeValidPeriodAfterCreate() {
24 | final int invalidValidPeriod = -1 * Math.abs(ThreadLocalRandom.current().nextInt());
25 | final ExpirableBloomFilterConfig config = new ExpirableBloomFilterConfig();
26 | final Duration old = config.validPeriodAfterCreate();
27 | assertThatThrownBy(() -> config.setValidPeriodAfterCreate(Duration.ofSeconds(invalidValidPeriod)))
28 | .isInstanceOf(BadParameterException.class)
29 | .hasMessageContaining("invalid parameter");
30 | assertThat(config.validPeriodAfterCreate()).isSameAs(old);
31 | }
32 |
33 | @Test
34 | public void testSetZeroValidPeriodAfterCreate() {
35 | final ExpirableBloomFilterConfig config = new ExpirableBloomFilterConfig();
36 | final Duration old = config.validPeriodAfterCreate();
37 | assertThatThrownBy(() -> config.setValidPeriodAfterCreate(Duration.ofSeconds(0)))
38 | .isInstanceOf(BadParameterException.class)
39 | .hasMessageContaining("invalid parameter");
40 |
41 | assertThat(config.validPeriodAfterCreate()).isEqualTo(old);
42 | }
43 |
44 | @Test
45 | public void testDefaultValidPeriodAfterAccess() {
46 | final ExpirableBloomFilterConfig config = new ExpirableBloomFilterConfig();
47 | assertThat(config.validPeriodAfterCreate()).isEqualTo(Configuration.defaultValidPeriodAfterCreate());
48 | assertThat(config.validPeriodAfterAccess()).isNull();
49 | }
50 |
51 | @Test
52 | public void testGetAndSetValidPeriodAfterAccess() {
53 | final int expectedValidPeriodAfterAccess = ThreadLocalRandom.current().nextInt(0, Integer.MAX_VALUE);
54 | final ExpirableBloomFilterConfig config = new ExpirableBloomFilterConfig();
55 |
56 | assertThat(config.setValidPeriodAfterAccess(Duration.ofSeconds(expectedValidPeriodAfterAccess))).isSameAs(config);
57 | assertThat(config.validPeriodAfterAccess()).isEqualTo(Duration.ofSeconds(expectedValidPeriodAfterAccess));
58 | }
59 |
60 | @Test
61 | public void testSetNegativeValidPeriodAfterAccess() {
62 | final int invalidValidPeriod = -1 * Math.abs(ThreadLocalRandom.current().nextInt());
63 | final ExpirableBloomFilterConfig config = new ExpirableBloomFilterConfig();
64 | assertThatThrownBy(() -> config.setValidPeriodAfterAccess(Duration.ofSeconds(invalidValidPeriod)))
65 | .isInstanceOf(BadParameterException.class)
66 | .hasMessageContaining("invalid parameter");
67 | assertThat(config.validPeriodAfterAccess()).isNull();
68 | }
69 |
70 | @Test
71 | public void testSetZeroValidPeriodAfterAccess() {
72 | final ExpirableBloomFilterConfig config = new ExpirableBloomFilterConfig();
73 | assertThatThrownBy(() -> config.setValidPeriodAfterAccess(Duration.ofSeconds(0)))
74 | .isInstanceOf(BadParameterException.class)
75 | .hasMessageContaining("invalid parameter");
76 |
77 | assertThat(config.validPeriodAfterAccess()).isNull();
78 | }
79 |
80 | @Test
81 | public void testEquals() {
82 | final int validPeriod = ThreadLocalRandom.current().nextInt(0, Integer.MAX_VALUE);
83 | final double fpp = ThreadLocalRandom.current().nextDouble(0.0001, 1);
84 | final int expectedInsertions = ThreadLocalRandom.current().nextInt(1, Integer.MAX_VALUE);
85 | final ExpirableBloomFilterConfig filterA = new ExpirableBloomFilterConfig()
86 | .setValidPeriodAfterCreate(Duration.ofSeconds(validPeriod))
87 | .setFpp(fpp)
88 | .setExpectedInsertions(expectedInsertions);
89 |
90 | final ExpirableBloomFilterConfig filterB = new ExpirableBloomFilterConfig()
91 | .setValidPeriodAfterCreate(Duration.ofSeconds(validPeriod))
92 | .setFpp(fpp)
93 | .setExpectedInsertions(expectedInsertions);
94 |
95 | assertThat(filterA).isEqualTo(filterB);
96 | }
97 |
98 | @Test
99 | public void testHashCode() {
100 | final int validPeriod = ThreadLocalRandom.current().nextInt(0, Integer.MAX_VALUE);
101 | final double fpp = ThreadLocalRandom.current().nextDouble(0.0001, 1);
102 | final int expectedInsertions = ThreadLocalRandom.current().nextInt(1, Integer.MAX_VALUE);
103 | final ExpirableBloomFilterConfig filterA = new ExpirableBloomFilterConfig()
104 | .setValidPeriodAfterCreate(Duration.ofSeconds(validPeriod))
105 | .setFpp(fpp)
106 | .setExpectedInsertions(expectedInsertions);
107 |
108 | final ExpirableBloomFilterConfig filterB = new ExpirableBloomFilterConfig()
109 | .setValidPeriodAfterCreate(Duration.ofSeconds(validPeriod))
110 | .setFpp(fpp)
111 | .setExpectedInsertions(expectedInsertions);
112 |
113 | assertThat(filterA.hashCode()).isEqualTo(filterB.hashCode());
114 | }
115 | }
--------------------------------------------------------------------------------
/filter-service-core/src/test/java/cn/leancloud/filter/service/FilterRecordInputStreamTest.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 | import org.apache.commons.io.FileUtils;
4 | import org.junit.After;
5 | import org.junit.Before;
6 | import org.junit.Test;
7 |
8 | import java.io.EOFException;
9 | import java.io.File;
10 | import java.io.IOException;
11 | import java.nio.ByteBuffer;
12 | import java.nio.channels.FileChannel;
13 | import java.nio.channels.GatheringByteChannel;
14 | import java.nio.file.StandardOpenOption;
15 | import java.util.List;
16 |
17 | import static cn.leancloud.filter.service.FilterRecord.*;
18 | import static cn.leancloud.filter.service.TestingUtils.generateFilterRecords;
19 | import static cn.leancloud.filter.service.TestingUtils.generateSingleFilterRecord;
20 | import static org.assertj.core.api.Assertions.assertThat;
21 | import static org.assertj.core.api.Assertions.assertThatThrownBy;
22 |
23 | public class FilterRecordInputStreamTest {
24 | private final GuavaBloomFilterFactory factory = new GuavaBloomFilterFactory();
25 |
26 | private FileChannel writeRecordChannel;
27 | private File tempFile;
28 | private String tempDir;
29 |
30 | @Before
31 | public void setUp() throws Exception {
32 | tempDir = System.getProperty("java.io.tmpdir", "/tmp") +
33 | File.separator + "filter_service_" + System.nanoTime();
34 | FileUtils.forceMkdir(new File(tempDir));
35 | tempFile = new File(tempDir + File.separator + "filter_record_test");
36 | writeRecordChannel = FileChannel.open(tempFile.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.READ);
37 | }
38 |
39 | @After
40 | public void tearDown() throws Exception {
41 | writeRecordChannel.close();
42 | FileUtils.forceDelete(new File(tempDir));
43 | }
44 |
45 | @Test
46 | public void testReadWriteMultiFilterRecord() throws Exception {
47 | final List> records = generateFilterRecords(100);
48 | for (FilterRecord record : records) {
49 | record.writeFullyTo(writeRecordChannel);
50 | }
51 |
52 | try (FilterRecordInputStream stream = new FilterRecordInputStream<>(tempFile.toPath(), factory)) {
53 | for (FilterRecord expectRecord : records) {
54 | assertThat(stream.nextFilterRecord()).isEqualTo(expectRecord);
55 | }
56 |
57 | assertThat(stream.nextFilterRecord()).isNull();
58 | }
59 | }
60 |
61 | @Test
62 | public void testShortReadFilterHeader() throws Exception {
63 | final FilterRecord record = generateSingleFilterRecord();
64 | record.writeFullyTo(writeRecordChannel);
65 | writeRecordChannel.truncate(HEADER_OVERHEAD - 1);
66 |
67 | try (FilterRecordInputStream stream = new FilterRecordInputStream<>(tempFile.toPath(), factory)) {
68 | assertThatThrownBy(stream::nextFilterRecord).hasMessage(UnfinishedFilterException.shortReadFilterHeader(tempFile.toString(), 1).getMessage());
69 | }
70 | }
71 |
72 | @Test
73 | public void testShortReadFilterBody() throws Exception {
74 | final FilterRecord record = generateSingleFilterRecord();
75 | record.writeFullyTo(writeRecordChannel);
76 | writeRecordChannel.truncate(writeRecordChannel.size() - 1);
77 |
78 | try (FilterRecordInputStream stream = new FilterRecordInputStream<>(tempFile.toPath(), factory)) {
79 | assertThatThrownBy(stream::nextFilterRecord).hasMessage(UnfinishedFilterException.shortReadFilterBody(tempFile.toString(), 1).getMessage());
80 | }
81 | }
82 |
83 | @Test
84 | public void testBadMagic() throws Exception {
85 | final FilterRecord record = generateSingleFilterRecord();
86 | record.writeFullyTo(writeRecordChannel);
87 |
88 | overwriteFirstFilterRecordMagic((byte) 101);
89 |
90 | try (FilterRecordInputStream stream = new FilterRecordInputStream<>(tempFile.toPath(), factory)) {
91 | assertThatThrownBy(stream::nextFilterRecord)
92 | .isInstanceOf(InvalidFilterException.class)
93 | .hasMessageContaining("read unknown Magic: 101 from position");
94 | }
95 | }
96 |
97 | @Test
98 | public void testBadCrc() throws Exception {
99 | final FilterRecord record = generateSingleFilterRecord();
100 | record.writeFullyTo(writeRecordChannel);
101 |
102 | overwriteFirstFilterRecordCrc(101);
103 |
104 | try (FilterRecordInputStream stream = new FilterRecordInputStream<>(tempFile.toPath(), factory)) {
105 | assertThatThrownBy(stream::nextFilterRecord)
106 | .isInstanceOf(InvalidFilterException.class)
107 | .hasMessageContaining("got unmatched crc when read filter from position");
108 | }
109 | }
110 |
111 | @Test
112 | public void testEOF() throws Exception {
113 | final FilterRecord record = generateSingleFilterRecord();
114 | record.writeFullyTo(writeRecordChannel);
115 |
116 | try (FilterRecordInputStream stream = new FilterRecordInputStream<>(tempFile.toPath(), factory)) {
117 | writeRecordChannel.truncate(writeRecordChannel.size() - 1);
118 | assertThatThrownBy(stream::nextFilterRecord)
119 | .isInstanceOf(EOFException.class)
120 | .hasMessageContaining("Failed to read `FilterRecord` from file channel");
121 | }
122 | }
123 |
124 | private void overwriteFirstFilterRecordMagic(byte magic) throws IOException {
125 | ByteBuffer buffer = readFirstHeader();
126 | buffer.flip();
127 | buffer.put(MAGIC_OFFSET, magic);
128 | writeRecordChannel.position(0);
129 | writeFullyTo(writeRecordChannel, buffer);
130 | }
131 |
132 | private void overwriteFirstFilterRecordCrc(int crc) throws IOException {
133 | ByteBuffer buffer = readFirstHeader();
134 | buffer.flip();
135 | buffer.putInt(CRC_OFFSET, crc);
136 | writeRecordChannel.position(0);
137 | writeFullyTo(writeRecordChannel, buffer);
138 | }
139 |
140 | private int writeFullyTo(GatheringByteChannel channel, ByteBuffer buffer) throws IOException {
141 | buffer.mark();
142 | int written = 0;
143 | while (written < buffer.limit()) {
144 | written = channel.write(buffer);
145 | }
146 | buffer.reset();
147 | return written;
148 | }
149 |
150 | private ByteBuffer readFirstHeader() throws IOException {
151 | ByteBuffer buffer = ByteBuffer.allocate(HEADER_OVERHEAD);
152 | FilterRecordInputStream.readFullyOrFail(writeRecordChannel, buffer, 0);
153 | return buffer;
154 | }
155 | }
--------------------------------------------------------------------------------
/filter-service-core/src/test/java/cn/leancloud/filter/service/FilterRecordTest.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 | import org.apache.commons.io.FileUtils;
4 | import org.junit.After;
5 | import org.junit.Before;
6 | import org.junit.Test;
7 |
8 | import java.io.File;
9 | import java.nio.channels.FileChannel;
10 | import java.nio.file.StandardOpenOption;
11 | import java.time.Duration;
12 | import java.time.ZoneOffset;
13 | import java.time.ZonedDateTime;
14 |
15 | import static org.assertj.core.api.Assertions.assertThat;
16 |
17 | public class FilterRecordTest {
18 | private static final Duration validPeriodAfterAccess = Duration.ofSeconds(3);
19 | private static final int expectedInsertions = 1000000;
20 | private static final double fpp = 0.0001;
21 | private static final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC);
22 | private static final ZonedDateTime expiration = creation.plus(Duration.ofSeconds(10));
23 | private static final String testingFilterName = "testing_filter";
24 |
25 | private File tempFile;
26 | private String tempDir;
27 |
28 | @Before
29 | public void setUp() throws Exception {
30 | tempDir = System.getProperty("java.io.tmpdir", "/tmp") +
31 | File.separator + "filter_service_" + System.nanoTime();
32 | FileUtils.forceMkdir(new File(tempDir));
33 | tempFile = new File(tempDir + File.separator + "filter_record_test");
34 | }
35 |
36 | @After
37 | public void tearDown() throws Exception {
38 | FileUtils.forceDelete(new File(tempDir));
39 | }
40 |
41 | @Test
42 | public void testReadWriteFilterRecord() throws Exception {
43 | final GuavaBloomFilter filter = new GuavaBloomFilter(
44 | expectedInsertions,
45 | fpp,
46 | creation,
47 | expiration,
48 | validPeriodAfterAccess);
49 | final FilterRecord record = new FilterRecord<>(testingFilterName, filter);
50 | final FileChannel channel = FileChannel.open(tempFile.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.READ);
51 |
52 | record.writeFullyTo(channel);
53 |
54 | final FilterRecordInputStream stream = new FilterRecordInputStream<>(tempFile.toPath(), new GuavaBloomFilterFactory());
55 | assertThat(stream.nextFilterRecord()).isEqualTo(new FilterRecord<>(testingFilterName, filter));
56 | }
57 |
58 | @Test
59 | public void testHashAndEquals() {
60 | final GuavaBloomFilter filter = new GuavaBloomFilter(
61 | expectedInsertions,
62 | fpp,
63 | creation,
64 | expiration,
65 | validPeriodAfterAccess);
66 | final FilterRecord record = new FilterRecord<>(testingFilterName, filter);
67 | final FilterRecord record2 = new FilterRecord<>(testingFilterName, filter);
68 | assertThat(record.hashCode()).isEqualTo(record2.hashCode());
69 | assertThat(record.equals(record2)).isTrue();
70 | }
71 |
72 | @Test
73 | public void testHashAndEquals2() {
74 | final GuavaBloomFilter filter = new GuavaBloomFilter(
75 | expectedInsertions,
76 | fpp,
77 | creation,
78 | expiration,
79 | validPeriodAfterAccess);
80 | final FilterRecord record = new FilterRecord<>("record1", filter);
81 | final FilterRecord record2 = new FilterRecord<>("record2", filter);
82 | assertThat(record.hashCode()).isNotEqualTo(record2.hashCode());
83 | assertThat(record.equals(record2)).isFalse();
84 | }
85 |
86 | @Test
87 | public void testHashAndEquals3() {
88 | final FilterRecord record = new FilterRecord<>(testingFilterName,
89 | new GuavaBloomFilter(
90 | expectedInsertions,
91 | 0.01,
92 | creation,
93 | expiration,
94 | validPeriodAfterAccess));
95 | final FilterRecord record2 = new FilterRecord<>(testingFilterName,
96 | new GuavaBloomFilter(
97 | expectedInsertions,
98 | 0.001,
99 | creation,
100 | expiration,
101 | validPeriodAfterAccess));
102 | assertThat(record.hashCode()).isNotEqualTo(record2.hashCode());
103 | assertThat(record.equals(record2)).isFalse();
104 | }
105 | }
--------------------------------------------------------------------------------
/filter-service-core/src/test/java/cn/leancloud/filter/service/FilterServiceFileUtilsTest.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 |
4 | import org.apache.commons.io.FileUtils;
5 | import org.junit.Test;
6 |
7 | import java.io.File;
8 | import java.nio.file.NoSuchFileException;
9 | import java.nio.file.Path;
10 | import java.nio.file.Paths;
11 |
12 | import static org.assertj.core.api.Assertions.assertThatThrownBy;
13 |
14 | public class FilterServiceFileUtilsTest {
15 |
16 | @Test
17 | public void atomicMoveWithFallbackFailed() throws Exception {
18 | final String tempDir = System.getProperty("java.io.tmpdir", "/tmp") +
19 | File.separator + "filter_service_" + System.nanoTime();
20 | FileUtils.forceMkdir(new File(tempDir));
21 | Path a = Paths.get(tempDir).resolve("path_a");
22 | Path b = Paths.get(tempDir).resolve("path_b");
23 | assertThatThrownBy(() -> FilterServiceFileUtils.atomicMoveWithFallback(a, b))
24 | .isInstanceOf(NoSuchFileException.class);
25 | FileUtils.forceDelete(new File(tempDir));
26 | }
27 | }
--------------------------------------------------------------------------------
/filter-service-core/src/test/java/cn/leancloud/filter/service/GuavaBloomFilterTest.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import org.junit.Test;
5 |
6 | import java.io.ByteArrayInputStream;
7 | import java.io.ByteArrayOutputStream;
8 | import java.time.Duration;
9 | import java.time.ZoneOffset;
10 | import java.time.ZonedDateTime;
11 |
12 | import static org.assertj.core.api.Assertions.assertThat;
13 |
14 | public class GuavaBloomFilterTest {
15 | private static final GuavaBloomFilterFactory testingFactory = new GuavaBloomFilterFactory();
16 | private static final ExpirableBloomFilterConfig defaultTestingConfig = new ExpirableBloomFilterConfig();
17 |
18 | @Test
19 | public void testGetters() {
20 | final Duration validPeriodAfterAccess = Duration.ofSeconds(3);
21 | final int expectedInsertions = 1000000;
22 | final double fpp = 0.0001;
23 | final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC);
24 | final ZonedDateTime expiration = creation.plus(Duration.ofSeconds(10));
25 | final GuavaBloomFilter filter = new GuavaBloomFilter(
26 | expectedInsertions,
27 | fpp,
28 | creation,
29 | expiration,
30 | validPeriodAfterAccess);
31 |
32 | assertThat(filter.fpp()).isEqualTo(fpp);
33 | assertThat(filter.expectedInsertions()).isEqualTo(expectedInsertions);
34 | assertThat(filter.expiration()).isEqualTo(expiration);
35 | assertThat(filter.validPeriodAfterAccess()).isEqualTo(validPeriodAfterAccess);
36 | assertThat(filter.created()).isEqualTo(creation);
37 | assertThat(filter.expired()).isFalse();
38 | }
39 |
40 | @Test
41 | public void testMightContain() {
42 | final String testingValue = "SomeValue";
43 | final GuavaBloomFilter filter = testingFactory.createFilter(defaultTestingConfig);
44 | assertThat(filter.mightContain(testingValue)).isFalse();
45 | assertThat(filter.set(testingValue)).isTrue();
46 | assertThat(filter.mightContain(testingValue)).isTrue();
47 | }
48 |
49 | @Test
50 | public void testExpiredFilter() {
51 | final AdjustableTimer timer = new AdjustableTimer();
52 | final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC);
53 | final ZonedDateTime expiration = creation.plusSeconds(10);
54 | final GuavaBloomFilter filter = new GuavaBloomFilter(
55 | 1000,
56 | 0.001,
57 | creation,
58 | expiration,
59 | null,
60 | timer);
61 | timer.setNow(expiration);
62 | assertThat(filter.expired()).isFalse();
63 | timer.setNow(expiration.plusSeconds(1));
64 | assertThat(filter.expired()).isTrue();
65 | }
66 |
67 | @Test
68 | public void testExtendExpirationOnSet() {
69 | final String testingValue = "SomeValue";
70 | final AdjustableTimer timer = new AdjustableTimer();
71 | final Duration validPeriodAfterAccess = Duration.ofSeconds(5);
72 | final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC);
73 | final ZonedDateTime expiration = creation.plus(validPeriodAfterAccess);
74 | final GuavaBloomFilter filter = new GuavaBloomFilter(
75 | 1000,
76 | 0.001,
77 | creation,
78 | expiration,
79 | validPeriodAfterAccess,
80 | timer);
81 | timer.setNow(expiration);
82 | assertThat(filter.expired()).isFalse();
83 | filter.set(testingValue);
84 | timer.setNow(expiration.plus(Duration.ofSeconds(1)));
85 | assertThat(filter.expired()).isFalse();
86 | }
87 |
88 | @Test
89 | public void testExtendExpirationOnMightContain() {
90 | final String testingValue = "SomeValue";
91 | final AdjustableTimer timer = new AdjustableTimer();
92 | final Duration validPeriodAfterAccess = Duration.ofSeconds(5);
93 | final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC);
94 | final ZonedDateTime expiration = creation.plus(validPeriodAfterAccess);
95 | final GuavaBloomFilter filter = new GuavaBloomFilter(
96 | 1000,
97 | 0.001,
98 | creation,
99 | expiration,
100 | validPeriodAfterAccess,
101 | timer);
102 | timer.setNow(expiration);
103 | assertThat(filter.expired()).isFalse();
104 | filter.mightContain(testingValue);
105 | timer.setNow(expiration.plus(Duration.ofSeconds(1)));
106 | assertThat(filter.expired()).isFalse();
107 | }
108 |
109 | @Test
110 | public void testToJson() throws Exception {
111 | final Duration validPeriodAfterAccess = Duration.ofSeconds(10);
112 | final ExpirableBloomFilterConfig config = new ExpirableBloomFilterConfig(defaultTestingConfig);
113 | config.setValidPeriodAfterAccess(validPeriodAfterAccess);
114 |
115 | final GuavaBloomFilter expectedFilter = testingFactory.createFilter(config);
116 | final ObjectMapper mapper = new ObjectMapper();
117 |
118 | final String json = mapper.valueToTree(expectedFilter).toString();
119 | final GuavaBloomFilter filter = new ObjectMapper().readerFor(GuavaBloomFilter.class).readValue(json);
120 | assertThat(filter).isEqualTo(expectedFilter);
121 | }
122 |
123 | @Test
124 | public void testSerializationWithoutValidPeriodAfterAccess() throws Exception {
125 | final int expectedInsertions = 1000000;
126 | final double fpp = 0.0001;
127 | final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC);
128 | final ZonedDateTime expiration = creation.plus(Duration.ofSeconds(10));
129 | final GuavaBloomFilter expect = new GuavaBloomFilter(
130 | expectedInsertions,
131 | fpp,
132 | creation,
133 | expiration,
134 | null);
135 |
136 | final ByteArrayOutputStream out = new ByteArrayOutputStream();
137 | expect.writeTo(out);
138 |
139 | final ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
140 | final GuavaBloomFilter actualFilter = GuavaBloomFilter.readFrom(in);
141 |
142 | assertThat(actualFilter).isEqualTo(expect);
143 | assertThat(actualFilter.fpp()).isEqualTo(fpp);
144 | assertThat(actualFilter.expectedInsertions()).isEqualTo(expectedInsertions);
145 | assertThat(actualFilter.expiration().toEpochSecond()).isEqualTo(expiration.toEpochSecond());
146 | assertThat(actualFilter.validPeriodAfterAccess()).isNull();
147 | assertThat(actualFilter.created().toEpochSecond()).isEqualTo(creation.toEpochSecond());
148 | assertThat(actualFilter.expired()).isFalse();
149 | }
150 |
151 | @Test
152 | public void testSerializationWithValidPeriodAfterAccess() throws Exception {
153 | final Duration validPeriodAfterAccess = Duration.ofSeconds(3);
154 | final int expectedInsertions = 1000000;
155 | final double fpp = 0.0001;
156 | final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC);
157 | final ZonedDateTime expiration = creation.plus(Duration.ofSeconds(10));
158 | final GuavaBloomFilter expect = new GuavaBloomFilter(
159 | expectedInsertions,
160 | fpp,
161 | creation,
162 | expiration,
163 | validPeriodAfterAccess);
164 |
165 | final ByteArrayOutputStream out = new ByteArrayOutputStream();
166 | expect.writeTo(out);
167 |
168 | final ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
169 | final GuavaBloomFilter actualFilter = testingFactory.readFrom(in);
170 |
171 | assertThat(actualFilter).isEqualTo(expect);
172 | assertThat(actualFilter.fpp()).isEqualTo(fpp);
173 | assertThat(actualFilter.expectedInsertions()).isEqualTo(expectedInsertions);
174 | assertThat(actualFilter.expiration().toEpochSecond()).isEqualTo(expiration.toEpochSecond());
175 | assertThat(actualFilter.validPeriodAfterAccess()).isEqualTo(validPeriodAfterAccess);
176 | assertThat(actualFilter.created().toEpochSecond()).isEqualTo(creation.toEpochSecond());
177 | assertThat(actualFilter.expired()).isFalse();
178 | }
179 |
180 | @Test
181 | public void testHashcodeAndEquals() {
182 | final Duration validPeriodAfterAccess = Duration.ofSeconds(3);
183 | final int expectedInsertions = 1000000;
184 | final double fpp = 0.0001;
185 | final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC);
186 | final ZonedDateTime expiration = creation.plus(Duration.ofSeconds(10));
187 | final GuavaBloomFilter filter = new GuavaBloomFilter(
188 | expectedInsertions,
189 | fpp,
190 | creation,
191 | expiration,
192 | validPeriodAfterAccess);
193 |
194 | final GuavaBloomFilter filter2 = new GuavaBloomFilter(
195 | expectedInsertions,
196 | fpp,
197 | creation,
198 | expiration,
199 | validPeriodAfterAccess);
200 |
201 | assertThat(filter.hashCode()).isEqualTo(filter2.hashCode());
202 | assertThat(filter.equals(filter2)).isTrue();
203 | }
204 |
205 | @Test
206 | public void testHashcodeAndEquals2() {
207 | final Duration validPeriodAfterAccess = Duration.ofSeconds(3);
208 | final double fpp = 0.0001;
209 | final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC);
210 | final ZonedDateTime expiration = creation.plus(Duration.ofSeconds(10));
211 | final GuavaBloomFilter filter = new GuavaBloomFilter(
212 | 1001,
213 | fpp,
214 | creation,
215 | expiration,
216 | validPeriodAfterAccess);
217 |
218 | final GuavaBloomFilter filter2 = new GuavaBloomFilter(
219 | 1002,
220 | fpp,
221 | creation,
222 | expiration,
223 | validPeriodAfterAccess);
224 |
225 | assertThat(filter.hashCode()).isNotEqualTo(filter2.hashCode());
226 | assertThat(filter.equals(filter2)).isFalse();
227 | }
228 | }
--------------------------------------------------------------------------------
/filter-service-core/src/test/java/cn/leancloud/filter/service/InvalidBloomFilterPurgatoryTest.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 | import org.junit.Test;
4 | import org.mockito.Mockito;
5 |
6 | import java.time.Duration;
7 | import java.time.ZoneOffset;
8 | import java.time.ZonedDateTime;
9 |
10 | import static org.assertj.core.api.Assertions.assertThat;
11 |
12 | public class InvalidBloomFilterPurgatoryTest {
13 | private static final String testingFilterName = "TestingFilterName";
14 | @Test
15 | public void testPurge() {
16 | final ExpirableBloomFilterConfig config = new ExpirableBloomFilterConfig();
17 | final ZonedDateTime creationTime = ZonedDateTime.now(ZoneOffset.UTC).minus(Duration.ofSeconds(10));
18 | final ZonedDateTime expirationTime = creationTime.plusSeconds(5);
19 | final GuavaBloomFilterFactory mockedFactory = Mockito.mock(GuavaBloomFilterFactory.class);
20 |
21 | Mockito.when(mockedFactory.createFilter(config))
22 | .thenReturn(new GuavaBloomFilter(
23 | Configuration.defaultExpectedInsertions(),
24 | Configuration.defaultFalsePositiveProbability(),
25 | creationTime,
26 | expirationTime,
27 | null));
28 |
29 | final BloomFilterManagerImpl manager = new BloomFilterManagerImpl<>(mockedFactory);
30 | final GuavaBloomFilter filter = manager.createFilter(testingFilterName, config).getFilter();
31 | final InvalidBloomFilterPurgatory purgatory =
32 | new InvalidBloomFilterPurgatory<>(manager);
33 |
34 | assertThat(filter.expired()).isTrue();
35 | assertThat(manager.getFilter(testingFilterName)).isSameAs(filter);
36 |
37 | purgatory.purge();
38 |
39 | assertThat(manager.getFilter(testingFilterName)).isNull();
40 | }
41 | }
--------------------------------------------------------------------------------
/filter-service-core/src/test/java/cn/leancloud/filter/service/PersistentFilterByOutputStreamBenchmark.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 | import cn.leancloud.filter.service.utils.ChecksumedBufferedOutputStream;
4 | import org.openjdk.jmh.annotations.*;
5 | import org.openjdk.jmh.runner.Runner;
6 | import org.openjdk.jmh.runner.options.Options;
7 | import org.openjdk.jmh.runner.options.OptionsBuilder;
8 | import org.openjdk.jmh.runner.options.TimeValue;
9 |
10 | import java.io.BufferedOutputStream;
11 | import java.io.ByteArrayOutputStream;
12 | import java.io.IOException;
13 | import java.nio.ByteBuffer;
14 | import java.nio.channels.Channels;
15 | import java.nio.channels.FileChannel;
16 | import java.nio.channels.GatheringByteChannel;
17 | import java.nio.file.Paths;
18 | import java.nio.file.StandardOpenOption;
19 | import java.time.Duration;
20 | import java.time.ZoneOffset;
21 | import java.time.ZonedDateTime;
22 | import java.util.concurrent.TimeUnit;
23 |
24 | @State(Scope.Thread)
25 | @BenchmarkMode({Mode.Throughput})
26 | @OutputTimeUnit(TimeUnit.SECONDS)
27 | @Threads(value = 1)
28 | public class PersistentFilterByOutputStreamBenchmark {
29 | GuavaBloomFilter filter;
30 | FileChannel fileChannel;
31 |
32 | @Setup
33 | public void setup() throws Exception {
34 | final Duration validPeriodAfterAccess = Duration.ofSeconds(3);
35 | final int expectedInsertions = 10000000;
36 | final double fpp = 0.001;
37 | final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC);
38 | final ZonedDateTime expiration = creation.plus(Duration.ofSeconds(10));
39 | filter = new GuavaBloomFilter(
40 | expectedInsertions,
41 | fpp,
42 | creation,
43 | expiration,
44 | validPeriodAfterAccess);
45 | fileChannel = FileChannel.open(Paths.get("/dev/null"), StandardOpenOption.CREATE,
46 | StandardOpenOption.WRITE, StandardOpenOption.READ);
47 | }
48 |
49 | @TearDown
50 | public void teardown() throws Exception {
51 | fileChannel.close();
52 | }
53 |
54 | @Benchmark
55 | public int testByteArrayOutputStream() throws Exception {
56 | final ByteArrayOutputStream out = new ByteArrayOutputStream();
57 | filter.writeTo(out);
58 | ByteBuffer buffer = ByteBuffer.wrap(out.toByteArray());
59 | return writeFullyTo(fileChannel, buffer);
60 | }
61 |
62 | private int writeFullyTo(GatheringByteChannel channel, ByteBuffer buffer) throws IOException {
63 | buffer.mark();
64 | int written = 0;
65 | while (written < buffer.limit()) {
66 | written = channel.write(buffer);
67 | }
68 | buffer.reset();
69 | return written;
70 | }
71 |
72 | @Benchmark
73 | public int testBufferedOutputStream() throws Exception {
74 | final long pos = fileChannel.position();
75 | final BufferedOutputStream bout = new BufferedOutputStream(Channels.newOutputStream(fileChannel), 102400);
76 | filter.writeTo(bout);
77 | bout.flush();
78 | return (int) (fileChannel.position() - pos);
79 | }
80 |
81 | @Benchmark
82 | public int testChecksumedBufferedOutputStream() throws Exception {
83 | final long pos = fileChannel.position();
84 | final ChecksumedBufferedOutputStream bout = new ChecksumedBufferedOutputStream(Channels.newOutputStream(fileChannel), 102400);
85 | filter.writeTo(bout);
86 | bout.flush();
87 | return (int) (fileChannel.position() - pos);
88 | }
89 |
90 | public static void main(String[] args) throws Exception {
91 | Options opt = new OptionsBuilder()
92 | .include(PersistentFilterByOutputStreamBenchmark.class.getSimpleName())
93 | .warmupIterations(3)
94 | .warmupTime(TimeValue.seconds(10))
95 | .measurementIterations(3)
96 | .measurementTime(TimeValue.seconds(10))
97 | .forks(1)
98 | .build();
99 |
100 | new Runner(opt).run();
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/filter-service-core/src/test/java/cn/leancloud/filter/service/PersistentFiltersJobTest.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 | import cn.leancloud.filter.service.Configuration.TriggerPersistenceCriteria;
4 | import io.micrometer.core.instrument.MeterRegistry;
5 | import io.micrometer.core.instrument.Timer;
6 | import org.junit.Before;
7 | import org.junit.Test;
8 |
9 | import java.io.IOException;
10 | import java.time.Duration;
11 | import java.util.ArrayList;
12 | import java.util.Collections;
13 | import java.util.List;
14 | import java.util.concurrent.ScheduledExecutorService;
15 | import java.util.concurrent.ScheduledFuture;
16 | import java.util.concurrent.TimeUnit;
17 | import java.util.concurrent.atomic.LongAdder;
18 |
19 | import static org.assertj.core.api.Assertions.assertThat;
20 | import static org.assertj.core.api.Assertions.assertThatThrownBy;
21 | import static org.mockito.ArgumentMatchers.*;
22 | import static org.mockito.Mockito.*;
23 | import static org.mockito.Mockito.times;
24 |
25 | @SuppressWarnings("unchecked")
26 | public class PersistentFiltersJobTest {
27 | private PersistentFiltersJob job;
28 | private BloomFilterManager bloomFilterManager;
29 | private PersistentManager persistentManager;
30 | private LongAdder filterUpdateTimesCounter;
31 | private TriggerPersistenceCriteria criteria = new TriggerPersistenceCriteria(Duration.ofSeconds(1), 10);
32 |
33 | @Before
34 | public void setUp() {
35 | bloomFilterManager = mock(BloomFilterManager.class);
36 | persistentManager = mock(PersistentManager.class);
37 | filterUpdateTimesCounter = new LongAdder();
38 | job = new PersistentFiltersJob(bloomFilterManager, persistentManager, filterUpdateTimesCounter, criteria);
39 | }
40 |
41 | @Test
42 | public void testCriteriaNotMeet() throws IOException {
43 | filterUpdateTimesCounter.reset();
44 | filterUpdateTimesCounter.add(5);
45 | job.run();
46 | assertThat(filterUpdateTimesCounter.sum()).isEqualTo(5);
47 | verify(persistentManager, never()).freezeAllFilters(bloomFilterManager);
48 | }
49 |
50 | @Test
51 | public void testPersistentWhenCriteriaMeet() throws IOException {
52 | filterUpdateTimesCounter.add(100);
53 | job.run();
54 | assertThat(filterUpdateTimesCounter.sum()).isZero();
55 | verify(persistentManager, times(1)).freezeAllFilters(bloomFilterManager);
56 | }
57 |
58 | @Test
59 | public void testPersistentThrowsException() throws IOException {
60 | final Exception exception = new IOException("expected exception");
61 | doThrow(exception).when(persistentManager).freezeAllFilters(bloomFilterManager);
62 | filterUpdateTimesCounter.add(100);
63 | // eat exception
64 | job.run();
65 | assertThat(filterUpdateTimesCounter.sum()).isZero();
66 | verify(persistentManager, times(1)).freezeAllFilters(bloomFilterManager);
67 | }
68 |
69 | @Test
70 | public void testPersistentThrowsError() throws IOException {
71 | final Error error = new Error("expected error");
72 | doThrow(error).when(persistentManager).freezeAllFilters(bloomFilterManager);
73 | filterUpdateTimesCounter.add(100);
74 |
75 | assertThatThrownBy(() -> job.run()).isSameAs(error);
76 | assertThat(filterUpdateTimesCounter.sum()).isZero();
77 | verify(persistentManager, times(1)).freezeAllFilters(bloomFilterManager);
78 | }
79 | }
--------------------------------------------------------------------------------
/filter-service-core/src/test/java/cn/leancloud/filter/service/PersistentManagerTest.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 | import org.apache.commons.io.FileUtils;
4 | import org.junit.After;
5 | import org.junit.Before;
6 | import org.junit.Test;
7 |
8 | import java.io.File;
9 | import java.io.IOException;
10 | import java.nio.channels.FileChannel;
11 | import java.nio.channels.OverlappingFileLockException;
12 | import java.nio.file.Path;
13 | import java.nio.file.Paths;
14 | import java.nio.file.StandardOpenOption;
15 | import java.util.ArrayList;
16 | import java.util.List;
17 |
18 | import static cn.leancloud.filter.service.TestingUtils.generateFilterRecords;
19 | import static cn.leancloud.filter.service.TestingUtils.generateInvalidFilter;
20 | import static org.assertj.core.api.Assertions.assertThat;
21 | import static org.assertj.core.api.Assertions.assertThatThrownBy;
22 | import static org.mockito.ArgumentMatchers.anyCollection;
23 | import static org.mockito.Mockito.*;
24 |
25 | @SuppressWarnings("unchecked")
26 | public class PersistentManagerTest {
27 | private BloomFilterFactory factory;
28 | private Path tempDirPath;
29 | private BloomFilterManager filterManager;
30 | private PersistentManager manager;
31 |
32 | @Before
33 | public void setUp() throws Exception {
34 | final String tempDir = System.getProperty("java.io.tmpdir", "/tmp") +
35 | File.separator + "filter_service_" + System.nanoTime();
36 | tempDirPath = Paths.get(tempDir);
37 | FileUtils.forceMkdir(tempDirPath.toFile());
38 | filterManager = mock(BloomFilterManager.class);
39 | factory = mock(GuavaBloomFilterFactory.class);
40 | when(factory.readFrom(any())).thenCallRealMethod();
41 | manager = new PersistentManager<>(tempDirPath);
42 | }
43 |
44 | @After
45 | public void tearDown() throws Exception {
46 | manager.close();
47 | FileUtils.forceDelete(tempDirPath.toFile());
48 | }
49 |
50 | @Test
51 | public void testPersistentDirIsFile() throws Exception {
52 | FileChannel.open(tempDirPath.resolve("plain_file"), StandardOpenOption.CREATE, StandardOpenOption.WRITE);
53 | assertThatThrownBy(() -> new PersistentManager<>(tempDirPath.resolve("plain_file")))
54 | .isInstanceOf(IllegalStateException.class)
55 | .hasMessageContaining("invalid persistent directory path, it's a regular file");
56 | }
57 |
58 | @Test
59 | public void testLockAndReleaseLock() throws Exception {
60 | final Path lockPath = tempDirPath.resolve("lock_path");
61 | final PersistentManager manager = new PersistentManager<>(lockPath);
62 |
63 | assertThatThrownBy(() -> new PersistentManager<>(lockPath))
64 | .isInstanceOf(OverlappingFileLockException.class);
65 |
66 | manager.close();
67 |
68 | new PersistentManager<>(lockPath);
69 | }
70 |
71 | @Test
72 | public void testMakeBaseDir() throws Exception {
73 | final Path newPath = tempDirPath.resolve("base_dir");
74 | assertThat(newPath.toFile().exists()).isFalse();
75 | new PersistentManager<>(newPath);
76 | assertThat(newPath.toFile().exists()).isTrue();
77 | }
78 |
79 | @Test
80 | public void freezeAllFilters() throws IOException {
81 | final List> records = generateFilterRecords(10);
82 | when(filterManager.iterator()).thenReturn(records.iterator());
83 |
84 | manager.freezeAllFilters(filterManager);
85 |
86 | try (FilterRecordInputStream stream = new FilterRecordInputStream<>(
87 | manager.persistentFilePath(),
88 | new GuavaBloomFilterFactory())) {
89 | for (FilterRecord expectRecord : records) {
90 | assertThat(stream.nextFilterRecord()).isEqualTo(expectRecord);
91 | }
92 |
93 | assertThat(stream.nextFilterRecord()).isNull();
94 | }
95 | }
96 |
97 | @Test
98 | public void testNoFilesToRecover() throws IOException {
99 | manager.recoverFilters(factory, true);
100 | verify(filterManager, never()).addFilters(anyCollection());
101 | }
102 |
103 | @Test
104 | public void testRecoverFiltersFromFileNormalCase() throws IOException {
105 | final List> records = generateFilterRecords(10);
106 | when(filterManager.iterator()).thenReturn(records.iterator());
107 |
108 | manager.freezeAllFilters(filterManager);
109 | assertThat(manager.recoverFilters(factory, false)).isEqualTo(records);
110 | }
111 |
112 | @Test
113 | public void testRecoverOnlyValidFiltersFromFile() throws IOException {
114 | final List> records = generateFilterRecords(10);
115 | final BloomFilter invalidFilter = generateInvalidFilter();
116 | records.add(new FilterRecord("Invalid_Filter", invalidFilter));
117 | when(filterManager.iterator()).thenReturn(records.iterator());
118 |
119 | manager.freezeAllFilters(filterManager);
120 | assertThat(manager.recoverFilters(factory, false))
121 | .isEqualTo(records.subList(0, records.size() - 1));
122 | }
123 |
124 | @Test
125 | public void testDoNotAllowRecoverFromCorruptedFile() throws IOException {
126 | final List> records = generateFilterRecords(10);
127 | when(filterManager.iterator()).thenReturn(records.iterator());
128 | manager.freezeAllFilters(filterManager);
129 | try (FileChannel channel = FileChannel.open(manager.persistentFilePath(), StandardOpenOption.WRITE)) {
130 | channel.truncate(channel.size() - 1);
131 |
132 | assertThatThrownBy(() -> manager.recoverFilters(factory, false))
133 | .hasMessageContaining("failed to recover filters from:")
134 | .isInstanceOf(PersistentStorageException.class);
135 | }
136 | }
137 |
138 | @Test
139 | public void testAllowRecoverFromCorruptedFile() throws IOException {
140 | final List> records = generateFilterRecords(10);
141 | when(filterManager.iterator()).thenReturn(records.iterator());
142 | manager.freezeAllFilters(filterManager);
143 | try (FileChannel channel = FileChannel.open(manager.persistentFilePath(), StandardOpenOption.WRITE)) {
144 | channel.truncate(channel.size() - 1);
145 |
146 | assertThat(manager.recoverFilters(factory, true))
147 | .isEqualTo(records.subList(0, records.size() - 1));
148 | }
149 | }
150 |
151 | @Test
152 | public void testRecoverOnlyFromTemporaryFile() throws IOException {
153 | final List> records = generateFilterRecords(10);
154 | when(filterManager.iterator()).thenReturn(records.iterator());
155 |
156 | manager.freezeAllFilters(filterManager);
157 | FilterServiceFileUtils.atomicMoveWithFallback(manager.persistentFilePath(), manager.temporaryPersistentFilePath());
158 |
159 | assertThat(manager.recoverFilters(factory, false)).isEqualTo(records);
160 | }
161 |
162 | @Test
163 | public void testRecoverFromTemporaryFileAndNormalFile() throws IOException {
164 | final Path temporaryPath = manager.persistentFilePath().resolveSibling("tmp.bak");
165 | final List> records = generateFilterRecords(20);
166 | final List> expected = new ArrayList<>();
167 | expected.addAll(records);
168 | expected.addAll(records.subList(10, 20));
169 |
170 | // prepare temporary file
171 | when(filterManager.iterator()).thenReturn(records.subList(10, 20).iterator());
172 | manager.freezeAllFilters(filterManager);
173 | FilterServiceFileUtils.atomicMoveWithFallback(manager.persistentFilePath(), temporaryPath);
174 |
175 | // prepare normal file
176 | when(filterManager.iterator()).thenReturn(records.iterator());
177 | manager.freezeAllFilters(filterManager);
178 |
179 | FilterServiceFileUtils.atomicMoveWithFallback(temporaryPath, manager.temporaryPersistentFilePath());
180 | assertThat(manager.recoverFilters(factory, false)).isEqualTo(expected);
181 | }
182 |
183 | @Test
184 | public void testRecoverFromTemporaryFileFailed() throws IOException {
185 | final Path temporaryPath = manager.persistentFilePath().resolveSibling("tmp.bak");
186 | final List> records = generateFilterRecords(20);
187 |
188 | // prepare temporary file
189 | when(filterManager.iterator()).thenReturn(generateFilterRecords(50, 20).iterator());
190 | manager.freezeAllFilters(filterManager);
191 | FilterServiceFileUtils.atomicMoveWithFallback(manager.persistentFilePath(), temporaryPath);
192 |
193 | // prepare normal file
194 | when(filterManager.iterator()).thenReturn(records.iterator());
195 | manager.freezeAllFilters(filterManager);
196 |
197 | FilterServiceFileUtils.atomicMoveWithFallback(temporaryPath, manager.temporaryPersistentFilePath());
198 | try (FileChannel channel = FileChannel.open(manager.temporaryPersistentFilePath(), StandardOpenOption.WRITE)) {
199 | channel.truncate(channel.size() - 1);
200 | assertThat(manager.recoverFilters(factory, false)).isEqualTo(records);
201 | }
202 | }
203 | }
--------------------------------------------------------------------------------
/filter-service-core/src/test/java/cn/leancloud/filter/service/PurgeFiltersJobTest.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 | import org.junit.Before;
4 | import org.junit.Test;
5 |
6 | import static org.mockito.Mockito.*;
7 |
8 | public class PurgeFiltersJobTest {
9 | private Purgatory purgatory;
10 | private PurgeFiltersJob job;
11 |
12 | @Before
13 | public void setUp() {
14 | purgatory = mock(Purgatory.class);
15 | job = new PurgeFiltersJob(purgatory);
16 | }
17 |
18 | @Test
19 | public void testPurge() {
20 | doNothing().when(purgatory).purge();
21 | job.run();
22 | verify(purgatory, times(1)).purge();
23 | }
24 |
25 | @Test
26 | public void testPurgeThrowsException() {
27 | final RuntimeException ex = new RuntimeException("expected exception");
28 | doThrow(ex).when(purgatory).purge();
29 | job.run();
30 | verify(purgatory, times(1)).purge();
31 | }
32 | }
--------------------------------------------------------------------------------
/filter-service-core/src/test/java/cn/leancloud/filter/service/ServerOptionsTest.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.assertj.core.api.Assertions.assertThat;
6 |
7 | public class ServerOptionsTest {
8 | @Test
9 | public void testServerOptions() {
10 | final String configFilePath = "/mnt/avos/logs";
11 | final int port = 10101;
12 | final boolean docService = true;
13 | ServerOptions options = new ServerOptions(configFilePath, port, docService);
14 | assertThat(options.configFilePath()).isEqualTo(configFilePath);
15 | assertThat(options.port()).isEqualTo(port);
16 | assertThat(options.docServiceEnabled()).isTrue();
17 | }
18 |
19 | }
--------------------------------------------------------------------------------
/filter-service-core/src/test/java/cn/leancloud/filter/service/TestingUtils.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service;
2 |
3 | import java.time.Duration;
4 | import java.time.ZoneOffset;
5 | import java.time.ZonedDateTime;
6 | import java.util.ArrayList;
7 | import java.util.List;
8 |
9 | public final class TestingUtils {
10 | private static final Duration validPeriodAfterAccess = Duration.ofSeconds(3);
11 | private static final int expectedInsertions = 1000000;
12 | private static final double fpp = 0.0001;
13 | private static final String testingFilterName = "testing_filter";
14 |
15 | public static String numberString(Number num) {
16 | return "" + num;
17 | }
18 |
19 | public static FilterRecord generateSingleFilterRecord() {
20 | return generateFilterRecords(1).get(0);
21 | }
22 |
23 | public static List> generateFilterRecords(int size) {
24 | return generateFilterRecords(0, size);
25 | }
26 |
27 | public static List> generateFilterRecords(int startNumName, int size) {
28 | List> records = new ArrayList<>(size);
29 | for (int i = 0; i < size; ++i) {
30 | final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC);
31 | final ZonedDateTime expiration = creation.plus(Duration.ofSeconds(10));
32 | final GuavaBloomFilter filter = new GuavaBloomFilter(
33 | expectedInsertions + i,
34 | fpp,
35 | creation,
36 | expiration,
37 | validPeriodAfterAccess);
38 | final FilterRecord record = new FilterRecord<>(testingFilterName + "_" + i, filter);
39 | records.add(record);
40 | }
41 |
42 | return records;
43 | }
44 |
45 | public static GuavaBloomFilter generateInvalidFilter() {
46 | final ZonedDateTime creation = ZonedDateTime.now(ZoneOffset.UTC);
47 | final ZonedDateTime expiration = creation.minus(Duration.ofSeconds(10));
48 | return new GuavaBloomFilter(
49 | expectedInsertions,
50 | fpp,
51 | creation,
52 | expiration,
53 | validPeriodAfterAccess);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/filter-service-core/src/test/java/cn/leancloud/filter/service/utils/AbstractIteratorTest.java:
--------------------------------------------------------------------------------
1 | package cn.leancloud.filter.service.utils;
2 |
3 | import org.junit.Test;
4 |
5 | import javax.annotation.Nullable;
6 | import java.util.*;
7 |
8 | import static org.assertj.core.api.Assertions.assertThat;
9 | import static org.assertj.core.api.Assertions.assertThatThrownBy;
10 |
11 | public class AbstractIteratorTest {
12 | static class ListIterator extends AbstractIterator {
13 | private List list;
14 | private int position = 0;
15 |
16 | public ListIterator(List l) {
17 | this.list = l;
18 | }
19 |
20 | public T makeNext() {
21 | if (position < list.size())
22 | return list.get(position++);
23 | else
24 | return allDone();
25 | }
26 | }
27 |
28 | @Test
29 | public void testIterator() {
30 | final int max = 10;
31 | final List l = new ArrayList();
32 | for (int i = 0; i < max; i++)
33 | l.add(i);
34 | final ListIterator iter = new ListIterator(l);
35 | for (int i = 0; i < max; i++) {
36 | Integer value = i;
37 | assertThat(iter.peek()).isEqualTo(value);
38 | assertThat(iter.hasNext()).isTrue();
39 | assertThat(iter.next()).isEqualTo(value);
40 | }
41 | assertThat(iter.hasNext()).isFalse();
42 | }
43 |
44 | @Test
45 | public void testEmptyIterator() {
46 | final ListIterator