mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
Co-authored-by: denny <dennis.sheirer@gmail.com>
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.dsp.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Verifies the selection of one or two polyphase channel indexes for a tuner channel.
|
||||
*/
|
||||
public class ChannelCalculatorTest
|
||||
{
|
||||
private static final long CENTER = 100_000_000L;
|
||||
private static final double SAMPLE_RATE = 2_400_000.0;
|
||||
private static final int CHANNEL_COUNT = 96;
|
||||
private static final int CHANNEL_BANDWIDTH = 25000;
|
||||
|
||||
private static List<Integer> indexes(int channelCenterOffset, int frequencyOffset, int bandwidth)
|
||||
{
|
||||
return calculator().getChannelIndexes(new TunerChannel(frequency(channelCenterOffset, frequencyOffset), bandwidth));
|
||||
}
|
||||
|
||||
private static long frequency(int channelCenterOffset, int frequencyOffset)
|
||||
{
|
||||
return CENTER + (long)channelCenterOffset * CHANNEL_BANDWIDTH + frequencyOffset;
|
||||
}
|
||||
|
||||
private static ChannelCalculator calculator()
|
||||
{
|
||||
return new ChannelCalculator(SAMPLE_RATE, CHANNEL_COUNT, CENTER, 2.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: a narrow channel stays on one index until its band edge reaches the channelizer roll-off.
|
||||
*/
|
||||
@Test
|
||||
void narrowChannelMovesToTwoIndexesBeforeBandEdgeRollOff()
|
||||
{
|
||||
assertEquals(List.of(5), indexes(5, 2500, 12500));
|
||||
assertEquals(List.of(5, 6), indexes(5, 3500, 12500));
|
||||
assertEquals(List.of(4, 5), indexes(5, -3500, 12500));
|
||||
assertEquals(List.of(5), indexes(5, 6000, 5000));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: a wide channel near a polyphase channel center stays on one index, otherwise it uses the nearest boundary.
|
||||
*/
|
||||
@Test
|
||||
void wideChannelUsesOneIndexNearChannelCenter()
|
||||
{
|
||||
assertEquals(List.of(5), indexes(5, 0, 25000));
|
||||
assertEquals(List.of(5), indexes(5, 1000, 25000));
|
||||
assertEquals(List.of(5, 6), indexes(5, 3500, 25000));
|
||||
assertEquals(List.of(4, 5), indexes(5, -3500, 25000));
|
||||
assertEquals(List.of(5, 6), indexes(5, 12500, 25000));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: index pairs are ordered low to high in frequency across the center and wrap around indexes.
|
||||
*/
|
||||
@Test
|
||||
void indexPairsAreOrderedAcrossCenterAndWrapAround()
|
||||
{
|
||||
assertEquals(List.of(95, 0), indexes(0, -6000, 12500));
|
||||
assertEquals(List.of(0, 1), indexes(0, 6000, 12500));
|
||||
assertEquals(List.of(47, 48), indexes(47, 12500, 12500));
|
||||
assertEquals(List.of(48, 49), indexes(-47, -12500, 12500));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: every sourceable channel across the spectrum uses at most two adjacent indexes and the synthesized center
|
||||
* frequency is the boundary nearest to the channel.
|
||||
*/
|
||||
@Test
|
||||
void allChannelsUseAtMostTwoIndexesAroundNearestBoundary()
|
||||
{
|
||||
ChannelCalculator calculator = calculator();
|
||||
|
||||
for(int bandwidth : new int[]{3000, 6250, 12500, 15000, 25000})
|
||||
{
|
||||
long start = CENTER - (long)(SAMPLE_RATE / 2) + bandwidth;
|
||||
long stop = CENTER + (long)(SAMPLE_RATE / 2) - bandwidth;
|
||||
|
||||
for(long frequency = start; frequency <= stop; frequency += 730)
|
||||
{
|
||||
TunerChannel channel = new TunerChannel(frequency, bandwidth);
|
||||
List<Integer> indexes;
|
||||
|
||||
try
|
||||
{
|
||||
indexes = calculator.getChannelIndexes(channel);
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
continue; //Within the wrap around channel, which can't be sourced
|
||||
}
|
||||
|
||||
assertTrue(indexes.size() <= 2, "Too many indexes " + indexes + " for " + channel);
|
||||
|
||||
long error = Math.abs(calculator.getCenterFrequencyForIndexes(indexes) - frequency);
|
||||
assertTrue(error <= CHANNEL_BANDWIDTH / 2 + 1, "Center " + error + " Hz from " + channel);
|
||||
|
||||
if(indexes.size() == 2)
|
||||
{
|
||||
assertEquals((indexes.get(0) + 1) % CHANNEL_COUNT, indexes.get(1), "Not adjacent: " + indexes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: channels outside of the tuner bandwidth are still rejected.
|
||||
*/
|
||||
@Test
|
||||
void channelBeyondTunerBandwidthIsRejected()
|
||||
{
|
||||
assertThrows(IllegalArgumentException.class, () -> calculator().getChannelIndexes(
|
||||
new TunerChannel(CENTER + (long)(SAMPLE_RATE / 2), 12500)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.dsp.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import org.jtransforms.fft.FloatFFT_1D;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Verifies that the channel DFT produces the same channel values as the IFFT, both directly and through the threaded
|
||||
* channelizer, including across channel changes.
|
||||
*/
|
||||
public class ChannelDFTTest
|
||||
{
|
||||
private static final float TOLERANCE = 0.00001f;
|
||||
private static final int BLOCKS_PER_BATCH = 1024;
|
||||
private static final int TAPS_PER_CHANNEL = 9;
|
||||
private static final int[] CHANNEL_COUNTS = {2, 8, 72, 96, 100, 128, 400};
|
||||
|
||||
private static float[] randomSamples(int length, Random random)
|
||||
{
|
||||
float[] samples = new float[length];
|
||||
|
||||
for(int x = 0; x < samples.length; x++)
|
||||
{
|
||||
samples[x] = random.nextFloat() * 2.0f - 1.0f;
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
private static void assertMatchesIFFT(ChannelDFT channelDFT, int channelCount, int[] channels, Random random)
|
||||
{
|
||||
int blocks = 3;
|
||||
int blockLength = channelCount * 2;
|
||||
float[] input = randomSamples(blocks * blockLength, random);
|
||||
float[] expected = input.clone();
|
||||
FloatFFT_1D fft = new FloatFFT_1D(channelCount);
|
||||
|
||||
for(int block = 0; block < blocks; block++)
|
||||
{
|
||||
fft.complexInverse(expected, block * blockLength, true);
|
||||
}
|
||||
|
||||
float[] actual = input.clone();
|
||||
channelDFT.process(actual, channels);
|
||||
|
||||
for(int block = 0; block < blocks; block++)
|
||||
{
|
||||
for(int channel : channels)
|
||||
{
|
||||
int index = block * blockLength + channel * 2;
|
||||
assertEquals(expected[index], actual[index], TOLERANCE, "I channel " + channel + " of " + channelCount);
|
||||
assertEquals(expected[index + 1], actual[index + 1], TOLERANCE, "Q channel " + channel + " of " + channelCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: both implementations match the IFFT for each single channel and for sets of channels.
|
||||
*/
|
||||
@Test
|
||||
void implementationsMatchIFFT()
|
||||
{
|
||||
Random random = new Random(0xC0FFEEL);
|
||||
|
||||
for(int channelCount : CHANNEL_COUNTS)
|
||||
{
|
||||
for(ChannelDFT channelDFT : List.of(new ScalarChannelDFT(channelCount), new VectorChannelDFT(channelCount)))
|
||||
{
|
||||
for(int channel = 0; channel < channelCount; channel++)
|
||||
{
|
||||
assertMatchesIFFT(channelDFT, channelCount, new int[]{channel}, random);
|
||||
}
|
||||
|
||||
//All channels at once verifies that results don't overwrite the input of the other channels
|
||||
int[] all = new int[channelCount];
|
||||
Arrays.setAll(all, x -> x);
|
||||
assertMatchesIFFT(channelDFT, channelCount, all, random);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: each implementation limits use to channel counts that it can process faster than the IFFT.
|
||||
*/
|
||||
@Test
|
||||
void maximumChannelsAreAboveZero()
|
||||
{
|
||||
assertTrue(new ScalarChannelDFT(96).getMaximumChannels() > 0);
|
||||
assertTrue(new VectorChannelDFT(96).getMaximumChannels() > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: channels in use receive the same values as the IFFT through the threaded channelizer, on one index and on
|
||||
* two adjacent indexes. Channelizer outputs include the alternating block phase handling, so this also verifies
|
||||
* that both transforms see the same block order.
|
||||
*/
|
||||
@Test
|
||||
void channelizerMatchesIFFTForChannelsInUse() throws Exception
|
||||
{
|
||||
int channelCount = 96;
|
||||
float[] taps = taps(channelCount);
|
||||
Random random = new Random(1234);
|
||||
|
||||
for(long frequency : new long[]{100_125_000L, 100_137_500L})
|
||||
{
|
||||
ChannelCalculator calculator = new ChannelCalculator(2_400_000.0, channelCount, 100_000_000L, 2.0);
|
||||
List<float[]> sparse = new CopyOnWriteArrayList<>();
|
||||
List<float[]> full = new CopyOnWriteArrayList<>();
|
||||
ComplexPolyphaseChannelizerM2 sparseChannelizer = channelizer(taps, channelCount);
|
||||
ComplexPolyphaseChannelizerM2 fullChannelizer = channelizer(taps, channelCount);
|
||||
CapturingSource source = new CapturingSource(frequency, calculator, sparse);
|
||||
sparseChannelizer.addChannel(source);
|
||||
fullChannelizer.addChannel(new CapturingSource(frequency, calculator, full, channelCount));
|
||||
sparseChannelizer.start();
|
||||
fullChannelizer.start();
|
||||
|
||||
float[] samples = randomSamples(BLOCKS_PER_BATCH * channelCount * 3, random);
|
||||
sparseChannelizer.receive(new InterleavedComplexSamples(samples, 0));
|
||||
fullChannelizer.receive(new InterleavedComplexSamples(samples, 0));
|
||||
await(sparse, 3);
|
||||
await(full, 3);
|
||||
sparseChannelizer.stop();
|
||||
fullChannelizer.stop();
|
||||
|
||||
assertChannelsMatch(sparse, full, source.getOutputProcessorIndexes(), channelCount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: when an update moves a channel to a different index, the first batch after the update has valid results for
|
||||
* the new index.
|
||||
*/
|
||||
@Test
|
||||
void channelUpdateAppliesBeforeTransform() throws Exception
|
||||
{
|
||||
int channelCount = 96;
|
||||
float[] taps = taps(channelCount);
|
||||
long frequency = 100_125_000L;
|
||||
List<float[]> sparse = new CopyOnWriteArrayList<>();
|
||||
List<float[]> full = new CopyOnWriteArrayList<>();
|
||||
ComplexPolyphaseChannelizerM2 sparseChannelizer = channelizer(taps, channelCount);
|
||||
ComplexPolyphaseChannelizerM2 fullChannelizer = channelizer(taps, channelCount);
|
||||
ChannelCalculator calculator = new ChannelCalculator(2_400_000.0, channelCount, 100_000_000L, 2.0);
|
||||
CapturingSource source = new CapturingSource(frequency, calculator, sparse);
|
||||
sparseChannelizer.addChannel(source);
|
||||
fullChannelizer.addChannel(new CapturingSource(frequency, calculator, full, channelCount));
|
||||
sparseChannelizer.start();
|
||||
fullChannelizer.start();
|
||||
|
||||
ChannelCalculator retuned = new ChannelCalculator(2_400_000.0, channelCount, 100_050_000L, 2.0);
|
||||
source.updateOutputProcessor(retuned, new SynthesisFilterManager());
|
||||
|
||||
float[] samples = randomSamples(BLOCKS_PER_BATCH * channelCount, new Random(5678));
|
||||
sparseChannelizer.receive(new InterleavedComplexSamples(samples, 0));
|
||||
fullChannelizer.receive(new InterleavedComplexSamples(samples, 0));
|
||||
await(sparse, 1);
|
||||
await(full, 1);
|
||||
sparseChannelizer.stop();
|
||||
fullChannelizer.stop();
|
||||
|
||||
assertEquals(List.of(3), source.getOutputProcessorIndexes());
|
||||
assertChannelsMatch(sparse, full, source.getOutputProcessorIndexes(), channelCount);
|
||||
}
|
||||
|
||||
private static void assertChannelsMatch(List<float[]> actual, List<float[]> expected, List<Integer> indexes, int channelCount)
|
||||
{
|
||||
int blockLength = channelCount * 2;
|
||||
|
||||
for(int batch = 0; batch < expected.size(); batch++)
|
||||
{
|
||||
for(int index : indexes)
|
||||
{
|
||||
float[] expectedChannel = extract(expected.get(batch), index, blockLength);
|
||||
float[] actualChannel = extract(actual.get(batch), index, blockLength);
|
||||
assertArrayEquals(expectedChannel, actualChannel, TOLERANCE, "Channel " + index + " batch " + batch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static float[] extract(float[] samples, int index, int blockLength)
|
||||
{
|
||||
float[] channel = new float[samples.length / blockLength * 2];
|
||||
|
||||
for(int block = 0; block < channel.length / 2; block++)
|
||||
{
|
||||
channel[block * 2] = samples[block * blockLength + index * 2];
|
||||
channel[block * 2 + 1] = samples[block * blockLength + index * 2 + 1];
|
||||
}
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
private static float[] taps(int channelCount)
|
||||
{
|
||||
float[] taps = randomSamples(channelCount * TAPS_PER_CHANNEL, new Random(99));
|
||||
|
||||
for(int x = 0; x < taps.length; x++)
|
||||
{
|
||||
taps[x] /= channelCount;
|
||||
}
|
||||
|
||||
return taps;
|
||||
}
|
||||
|
||||
private static ComplexPolyphaseChannelizerM2 channelizer(float[] taps, int channelCount)
|
||||
{
|
||||
return new ComplexPolyphaseChannelizerM2(taps, 2_400_000, channelCount);
|
||||
}
|
||||
|
||||
private static void await(List<float[]> list, int size) throws Exception
|
||||
{
|
||||
long timeout = System.currentTimeMillis() + 5000;
|
||||
|
||||
while(list.size() < size && System.currentTimeMillis() < timeout)
|
||||
{
|
||||
Thread.sleep(2);
|
||||
}
|
||||
|
||||
assertEquals(size, list.size(), "Batches received");
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel source that captures a copy of each batch. A source can also claim more indexes than its channel, which
|
||||
* forces the channelizer to use the IFFT.
|
||||
*/
|
||||
private static class CapturingSource extends PolyphaseChannelSource
|
||||
{
|
||||
private final List<float[]> mCaptured;
|
||||
private final List<Integer> mForcedIndexes;
|
||||
|
||||
CapturingSource(long frequency, ChannelCalculator calculator, List<float[]> captured)
|
||||
{
|
||||
this(frequency, calculator, captured, 0);
|
||||
}
|
||||
|
||||
CapturingSource(long frequency, ChannelCalculator calculator, List<float[]> captured, int forcedIndexCount)
|
||||
{
|
||||
super(new TunerChannel(frequency, 12500), calculator, new SynthesisFilterManager(), sourceEvent -> {},
|
||||
"test", null);
|
||||
mCaptured = captured;
|
||||
mForcedIndexes = new ArrayList<>();
|
||||
|
||||
for(int x = 0; x < forcedIndexCount; x++)
|
||||
{
|
||||
mForcedIndexes.add(x);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Integer> getOutputProcessorIndexes()
|
||||
{
|
||||
return mForcedIndexes.isEmpty() ? super.getOutputProcessorIndexes() : mForcedIndexes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveChannelResults(ChannelResultsBatch channelResults)
|
||||
{
|
||||
mCaptured.add(channelResults.getSamples().clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.dsp.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.channelizer.output.OneChannelOutputProcessor;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Verifies the shared ownership of channel results batch sample arrays.
|
||||
*/
|
||||
public class ChannelResultsBatchTest
|
||||
{
|
||||
/**
|
||||
* Test: the sample array only returns to the pool after every reference is released.
|
||||
*/
|
||||
@Test
|
||||
void samplesReturnToPoolAfterLastRelease()
|
||||
{
|
||||
Queue<float[]> pool = new ArrayDeque<>();
|
||||
float[] samples = new float[8];
|
||||
ChannelResultsBatch batch = new ChannelResultsBatch(samples, 4, 0, pool);
|
||||
batch.retain();
|
||||
batch.retain();
|
||||
|
||||
batch.release();
|
||||
batch.release();
|
||||
assertTrue(pool.isEmpty(), "Returned to pool with an outstanding reference");
|
||||
|
||||
batch.release();
|
||||
assertSame(samples, pool.poll());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: blocks are derived from the array and block lengths.
|
||||
*/
|
||||
@Test
|
||||
void blockCountIsDerivedFromLengths()
|
||||
{
|
||||
assertEquals(2, new ChannelResultsBatch(new float[8], 4, 0, new ArrayDeque<>()).getBlockCount());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: an output processor that doesn't accept a batch because it isn't running doesn't keep a reference.
|
||||
*/
|
||||
@Test
|
||||
void stoppedOutputProcessorReleasesBatch()
|
||||
{
|
||||
Queue<float[]> pool = new ArrayDeque<>();
|
||||
ChannelResultsBatch batch = new ChannelResultsBatch(new float[8], 4, 0, pool);
|
||||
OneChannelOutputProcessor processor = new OneChannelOutputProcessor(50000.0, List.of(0), 1.0f, null, "test");
|
||||
|
||||
processor.receiveChannelResults(batch);
|
||||
batch.release();
|
||||
|
||||
assertEquals(1, pool.size());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.dsp.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.channelizer.output.OneChannelOutputProcessor;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* Verifies that channel results batches keep their own timestamp and configuration through the threaded dispatchers.
|
||||
*/
|
||||
public class ChannelResultsTimestampTest
|
||||
{
|
||||
private static final int BATCH_SIZE = 2048;
|
||||
private static final int CHANNEL_COUNT = 8;
|
||||
private static final int SAMPLES_PER_BATCH = 1024 * CHANNEL_COUNT / 2;
|
||||
|
||||
private static void awaitSize(List<?> list, int size) throws Exception
|
||||
{
|
||||
long timeout = System.currentTimeMillis() + 5000;
|
||||
|
||||
while(list.size() < size && System.currentTimeMillis() < timeout)
|
||||
{
|
||||
Thread.sleep(5);
|
||||
}
|
||||
}
|
||||
|
||||
private static ChannelResultsBatch batch(float i, float q, long timestamp)
|
||||
{
|
||||
float[] samples = new float[BATCH_SIZE * 4];
|
||||
|
||||
for(int x = 0; x < samples.length; x += 4)
|
||||
{
|
||||
samples[x] = i;
|
||||
samples[x + 1] = q;
|
||||
samples[x + 2] = -i;
|
||||
samples[x + 3] = -q;
|
||||
}
|
||||
|
||||
return new ChannelResultsBatch(samples, 4, timestamp, new ArrayDeque<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: each batch dispatched by the channelizer has the timestamp of the sample buffer that completed the batch,
|
||||
* even when a later sample buffer arrives before the batch is dispatched.
|
||||
*/
|
||||
@Test
|
||||
void channelizerDispatchesEachBatchWithItsOwnTimestamp() throws Exception
|
||||
{
|
||||
List<Long> timestamps = new CopyOnWriteArrayList<>();
|
||||
ComplexPolyphaseChannelizerM2 channelizer = new ComplexPolyphaseChannelizerM2(new float[CHANNEL_COUNT * 9],
|
||||
25000 * CHANNEL_COUNT, CHANNEL_COUNT)
|
||||
{
|
||||
@Override
|
||||
protected void dispatch(ChannelResultsBatch channelResults)
|
||||
{
|
||||
timestamps.add(channelResults.getTimestamp());
|
||||
}
|
||||
};
|
||||
|
||||
channelizer.start();
|
||||
|
||||
for(long timestamp : new long[]{10, 20, 30})
|
||||
{
|
||||
channelizer.receive(new InterleavedComplexSamples(new float[SAMPLES_PER_BATCH * 2], timestamp));
|
||||
}
|
||||
|
||||
awaitSize(timestamps, 3);
|
||||
channelizer.stop();
|
||||
|
||||
assertEquals(List.of(10L, 20L, 30L), timestamps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: assembled sample buffers have the timestamp of their batch, and a configuration change requested between
|
||||
* two batches takes effect at the start of the second batch, even when the first batch hasn't been processed yet.
|
||||
*/
|
||||
@Test
|
||||
void outputProcessorAppliesTimestampAndConfigurationPerBatch() throws Exception
|
||||
{
|
||||
List<ComplexSamples> output = new CopyOnWriteArrayList<>();
|
||||
OneChannelOutputProcessor processor = new OneChannelOutputProcessor(50000.0, List.of(0), 1.0f, null, "test");
|
||||
processor.setListener(output::add);
|
||||
|
||||
processor.start();
|
||||
processor.receiveChannelResults(batch(1.0f, 2.0f, 100));
|
||||
processor.setPolyphaseChannelIndices(List.of(1));
|
||||
processor.receiveChannelResults(batch(1.0f, 2.0f, 200));
|
||||
awaitSize(output, 2);
|
||||
processor.stop();
|
||||
|
||||
float[] expected = new float[BATCH_SIZE];
|
||||
assertEquals(100, output.get(0).timestamp());
|
||||
Arrays.fill(expected, 1.0f);
|
||||
assertArrayEquals(expected, output.get(0).i());
|
||||
assertEquals(200, output.get(1).timestamp());
|
||||
Arrays.fill(expected, -1.0f);
|
||||
assertArrayEquals(expected, output.get(1).i());
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,12 @@
|
||||
|
||||
package io.github.dsheirer.dsp.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import org.jtransforms.fft.FloatFFT_1D;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@@ -86,6 +90,94 @@ public class ComplexPolyphaseChannelizerM2Test
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: the channelizer's output is bit-for-bit identical to a reference that shifts all of its samples for each
|
||||
* block, across several wraps of the sample buffer window and with buffers that end part way through a block.
|
||||
*/
|
||||
@Test
|
||||
void slidingWindowMatchesShiftingReference() throws Exception
|
||||
{
|
||||
Random rng = new Random(0xC0FFEEL);
|
||||
|
||||
for(int channelCount : new int[]{2, 8, 96, 100, 400})
|
||||
{
|
||||
Harness harness = new Harness(channelCount, TAPS_PER_CHANNEL, rng);
|
||||
List<float[]> batches = new CopyOnWriteArrayList<>();
|
||||
ComplexPolyphaseChannelizerM2 channelizer = new ComplexPolyphaseChannelizerM2(harness.getCoefficients(),
|
||||
25000 * channelCount, channelCount)
|
||||
{
|
||||
@Override
|
||||
protected void dispatch(ChannelResultsBatch channelResults)
|
||||
{
|
||||
batches.add(channelResults.getSamples().clone());
|
||||
}
|
||||
};
|
||||
|
||||
int batchCount = 3;
|
||||
int blocksPerBatch = 1024;
|
||||
float[] samples = new float[batchCount * blocksPerBatch * channelCount];
|
||||
|
||||
for(int x = 0; x < samples.length; x++)
|
||||
{
|
||||
samples[x] = rng.nextFloat() * 2f - 1f;
|
||||
}
|
||||
|
||||
channelizer.start();
|
||||
int pointer = 0;
|
||||
|
||||
while(pointer < samples.length)
|
||||
{
|
||||
int length = Math.min(2 * (1 + rng.nextInt(channelCount * 3)), samples.length - pointer);
|
||||
channelizer.receive(new InterleavedComplexSamples(Arrays.copyOfRange(samples, pointer, pointer + length), 0));
|
||||
pointer += length;
|
||||
}
|
||||
|
||||
long timeout = System.currentTimeMillis() + 5000;
|
||||
|
||||
while(batches.size() < batchCount && System.currentTimeMillis() < timeout)
|
||||
{
|
||||
Thread.sleep(2);
|
||||
}
|
||||
|
||||
channelizer.stop();
|
||||
assertEquals(batchCount, batches.size(), "Batches (channelCount=" + channelCount + ")");
|
||||
|
||||
FloatFFT_1D fft = new FloatFFT_1D(channelCount);
|
||||
int blockLength = channelCount * 2;
|
||||
boolean topBlock = TAPS_PER_CHANNEL % 2 == 0;
|
||||
|
||||
for(int block = 0; block < batchCount * blocksPerBatch; block++)
|
||||
{
|
||||
harness.load(samples, block * channelCount, channelCount);
|
||||
float[] expected = harness.processFused(topBlock);
|
||||
topBlock = !topBlock;
|
||||
harness.shift(channelCount);
|
||||
fft.complexInverse(expected, true);
|
||||
|
||||
float[] batch = batches.get(block / blocksPerBatch);
|
||||
int offset = (block % blocksPerBatch) * blockLength;
|
||||
assertBitIdentical(expected, Arrays.copyOfRange(batch, offset, offset + blockLength), channelCount, 0, block);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: channel counts are even, have only factors of 2, 3 and 5, and are as large as possible for the minimum
|
||||
* channel bandwidth.
|
||||
*/
|
||||
@Test
|
||||
void channelCountIsAnEfficientIFFTSize()
|
||||
{
|
||||
assertEquals(96, ComplexPolyphaseChannelizerM2.getChannelCount(2_400_000.0));
|
||||
assertEquals(128, ComplexPolyphaseChannelizerM2.getChannelCount(3_200_000.0));
|
||||
assertEquals(400, ComplexPolyphaseChannelizerM2.getChannelCount(10_000_000.0));
|
||||
assertEquals(72, ComplexPolyphaseChannelizerM2.getChannelCount(1_920_000.0));
|
||||
assertEquals(100, ComplexPolyphaseChannelizerM2.getChannelCount(2_560_000.0));
|
||||
assertEquals(108, ComplexPolyphaseChannelizerM2.getChannelCount(2_880_000.0));
|
||||
assertEquals(240, ComplexPolyphaseChannelizerM2.getChannelCount(6_100_000.0));
|
||||
assertEquals(2, ComplexPolyphaseChannelizerM2.getChannelCount(50_000.0));
|
||||
}
|
||||
|
||||
private static void assertBitIdentical(float[] reference, float[] fused, int channelCount, int mode, int iteration)
|
||||
{
|
||||
assertEquals(reference.length, fused.length,
|
||||
@@ -116,6 +208,7 @@ public class ComplexPolyphaseChannelizerM2Test
|
||||
private final int[] mTopBlockMap; // length mSubChannelCount
|
||||
private final int[] mMiddleBlockMap; // length mSubChannelCount
|
||||
private final float[] mReusableAccumulator;
|
||||
private final float[] mCoefficients;
|
||||
|
||||
private Harness(int channelCount, int tapsPerChannel, Random rng)
|
||||
{
|
||||
@@ -129,6 +222,7 @@ public class ComplexPolyphaseChannelizerM2Test
|
||||
coefficients[i] = rng.nextFloat() * 2f - 1f;
|
||||
}
|
||||
|
||||
mCoefficients = coefficients;
|
||||
mInlineFilter = getAlignedFilter(coefficients, channelCount, tapsPerChannel);
|
||||
mInlineSamples = new float[mBufferLength];
|
||||
mTopBlockMap = getTopBlockMap(channelCount);
|
||||
@@ -136,6 +230,23 @@ public class ComplexPolyphaseChannelizerM2Test
|
||||
mReusableAccumulator = new float[mSubChannelCount];
|
||||
}
|
||||
|
||||
private float[] getCoefficients()
|
||||
{
|
||||
return mCoefficients;
|
||||
}
|
||||
|
||||
//Loads a block of samples to the start of the sample buffer
|
||||
private void load(float[] samples, int offset, int blockLength)
|
||||
{
|
||||
System.arraycopy(samples, offset, mInlineSamples, 0, blockLength);
|
||||
}
|
||||
|
||||
//Shifts all of the samples by one block
|
||||
private void shift(int blockLength)
|
||||
{
|
||||
System.arraycopy(mInlineSamples, 0, mInlineSamples, blockLength, mBufferLength - blockLength);
|
||||
}
|
||||
|
||||
//Reference: multiply the whole array into an interim buffer, then accumulate across taps (prior behavior).
|
||||
private float[] processReference(boolean topBlock)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.dsp.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Verifies that a failure in one polyphase channel source is contained to that source and is reported to its consumer.
|
||||
*/
|
||||
public class PolyphaseChannelSourceTest
|
||||
{
|
||||
private static final long CENTER = 100_000_000L;
|
||||
private static final double SAMPLE_RATE = 2_400_000.0;
|
||||
private static final int CHANNEL_COUNT = 96;
|
||||
|
||||
private static ChannelCalculator calculator(long center)
|
||||
{
|
||||
return new ChannelCalculator(SAMPLE_RATE, CHANNEL_COUNT, center, 2.0);
|
||||
}
|
||||
|
||||
private static ChannelResultsBatch batch()
|
||||
{
|
||||
return new ChannelResultsBatch(new float[CHANNEL_COUNT * 2], CHANNEL_COUNT * 2, 0, new ArrayDeque<>());
|
||||
}
|
||||
|
||||
private static PolyphaseChannelSource source()
|
||||
{
|
||||
return new PolyphaseChannelSource(new TunerChannel(CENTER + 125000, 12500), calculator(CENTER),
|
||||
new SynthesisFilterManager(), sourceEvent -> {}, "test", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: an output processor update failure doesn't throw to the channelizer, signals one error to the consumer and
|
||||
* stops this source from processing further results.
|
||||
*/
|
||||
@Test
|
||||
void failedOutputProcessorUpdateSignalsErrorOnce() throws Exception
|
||||
{
|
||||
CountDownLatch errorLatch = new CountDownLatch(1);
|
||||
AtomicInteger errorCount = new AtomicInteger();
|
||||
Listener<SourceEvent> consumer = sourceEvent -> {
|
||||
if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_ERROR_STATE)
|
||||
{
|
||||
errorCount.incrementAndGet();
|
||||
errorLatch.countDown();
|
||||
}
|
||||
};
|
||||
|
||||
PolyphaseChannelSource source = source();
|
||||
source.setSourceEventListener(consumer);
|
||||
|
||||
//Retune so that the channel is outside of the tuner bandwidth
|
||||
source.updateOutputProcessor(calculator(CENTER + 10_000_000L), new SynthesisFilterManager());
|
||||
|
||||
source.applyPendingOutputProcessorUpdate();
|
||||
source.receiveChannelResults(batch());
|
||||
source.applyPendingOutputProcessorUpdate();
|
||||
source.receiveChannelResults(batch());
|
||||
|
||||
assertTrue(errorLatch.await(5, TimeUnit.SECONDS), "Error was not signalled to the consumer");
|
||||
Thread.sleep(50);
|
||||
assertEquals(1, errorCount.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: an exception from one channel doesn't stop delivery of the results to the following channels.
|
||||
*/
|
||||
@Test
|
||||
void channelFailureDoesNotAffectFollowingChannels()
|
||||
{
|
||||
AtomicInteger received = new AtomicInteger();
|
||||
|
||||
PolyphaseChannelSource failing = new TestSource()
|
||||
{
|
||||
@Override
|
||||
public void receiveChannelResults(ChannelResultsBatch channelResults)
|
||||
{
|
||||
throw new IllegalStateException("Test failure");
|
||||
}
|
||||
};
|
||||
|
||||
PolyphaseChannelSource following = new TestSource()
|
||||
{
|
||||
@Override
|
||||
public void receiveChannelResults(ChannelResultsBatch channelResults)
|
||||
{
|
||||
received.incrementAndGet();
|
||||
}
|
||||
};
|
||||
|
||||
ComplexPolyphaseChannelizerM2 channelizer = new ComplexPolyphaseChannelizerM2(new float[CHANNEL_COUNT * 9],
|
||||
(int)SAMPLE_RATE, CHANNEL_COUNT);
|
||||
channelizer.addChannel(failing);
|
||||
channelizer.addChannel(following);
|
||||
|
||||
channelizer.prepareChannels();
|
||||
channelizer.dispatch(batch());
|
||||
|
||||
assertEquals(1, received.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Polyphase channel source for overriding the channel results processing.
|
||||
*/
|
||||
private static class TestSource extends PolyphaseChannelSource
|
||||
{
|
||||
TestSource()
|
||||
{
|
||||
super(new TunerChannel(CENTER + 125000, 12500), calculator(CENTER), new SynthesisFilterManager(),
|
||||
sourceEvent -> {}, "test", null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,9 @@ public class TwoChannelSynthesizerM2Test
|
||||
|
||||
/**
|
||||
* Test: tones at mirrored offsets from the boundary between two channels produce equal output levels, and a tone
|
||||
* exactly on the boundary produces the same level as one near the center of the composite.
|
||||
* exactly on the boundary produces the same level as one near the center of the composite. The pass band must
|
||||
* extend past the center of each channel so that a signal offset from the boundary doesn't lose its outer band edge,
|
||||
* and tones beyond the alias-free region of the composite must be rejected.
|
||||
*/
|
||||
@Test
|
||||
void boundaryResponseIsFlatAndSymmetric() throws Exception
|
||||
@@ -58,19 +60,17 @@ public class TwoChannelSynthesizerM2Test
|
||||
double center = gain(0.0, tapsPerChannel);
|
||||
assertEquals(1.0, center, 0.01, "Unity gain at channel boundary (taps per channel=" + tapsPerChannel + ")");
|
||||
|
||||
for(double offset : new double[]{0.1, 0.2, 0.3, 0.4})
|
||||
for(double offset : new double[]{0.1, 0.2, 0.3, 0.4, 0.5, 0.6})
|
||||
{
|
||||
double below = gain(-offset, tapsPerChannel);
|
||||
double above = gain(offset, tapsPerChannel);
|
||||
assertEquals(below, above, 0.01, "Asymmetric response at +/-" + offset +
|
||||
" channel bandwidths from boundary (taps per channel=" + tapsPerChannel + ")");
|
||||
|
||||
//Synthesis filter cutoff is 0.55 channel bandwidths, so only check flatness for the inner region
|
||||
if(offset <= 0.25)
|
||||
{
|
||||
assertTrue(below > 0.98, "Passband droop at " + offset + " channel bandwidths: " + below);
|
||||
}
|
||||
assertTrue(below > 0.98, "Passband droop at " + offset + " channel bandwidths: " + below);
|
||||
}
|
||||
|
||||
double stopBand = gain(0.9, tapsPerChannel);
|
||||
assertTrue(stopBand < 0.001, "Stop band leakage at 0.9 channel bandwidths: " + stopBand);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,9 +88,15 @@ public class TwoChannelSynthesizerM2Test
|
||||
CHANNEL_COUNT)
|
||||
{
|
||||
@Override
|
||||
protected void dispatch(List<float[]> channelResultsList)
|
||||
protected void dispatch(ChannelResultsBatch channelResults)
|
||||
{
|
||||
results.addAll(channelResultsList);
|
||||
float[] samples = channelResults.getSamples();
|
||||
int blockLength = channelResults.getBlockLength();
|
||||
|
||||
for(int offset = 0; offset < samples.length; offset += blockLength)
|
||||
{
|
||||
results.add(Arrays.copyOfRange(samples, offset, offset + blockLength));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -126,7 +132,7 @@ public class TwoChannelSynthesizerM2Test
|
||||
q2[x] = sign * result[2 * (LOWER_CHANNEL + 1) + 1];
|
||||
}
|
||||
|
||||
float[] filter = FilterFactory.getSincM2Synthesizer(2.0 * CHANNEL_BANDWIDTH, CHANNEL_BANDWIDTH, 2, 9);
|
||||
float[] filter = new SynthesisFilterManager().getFilter(2.0 * CHANNEL_BANDWIDTH, CHANNEL_BANDWIDTH, 2);
|
||||
ComplexSamples output = new TwoChannelSynthesizerM2(filter).process(new ComplexSamples(i1, q1, 0),
|
||||
new ComplexSamples(i2, q2, 0));
|
||||
|
||||
|
||||
128
src/test/java/io/github/dsheirer/util/DispatcherTest.java
Normal file
128
src/test/java/io/github/dsheirer/util/DispatcherTest.java
Normal file
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.util;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Verifies dispatcher queue limits.
|
||||
*/
|
||||
public class DispatcherTest
|
||||
{
|
||||
private static final int MAXIMUM_QUEUE_SIZE = 4;
|
||||
|
||||
/**
|
||||
* Sends the count of elements while the dispatcher's listener is blocked on the first element.
|
||||
* @return elements received by the listener after it is unblocked
|
||||
*/
|
||||
private static List<Integer> dispatchWhileBlocked(Dispatcher<Integer> dispatcher, int count) throws Exception
|
||||
{
|
||||
List<Integer> received = new CopyOnWriteArrayList<>();
|
||||
CountDownLatch blocked = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
|
||||
dispatcher.setListener(element -> {
|
||||
received.add(element);
|
||||
blocked.countDown();
|
||||
|
||||
try
|
||||
{
|
||||
release.await();
|
||||
}
|
||||
catch(InterruptedException ie)
|
||||
{
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
});
|
||||
|
||||
dispatcher.start();
|
||||
dispatcher.receive(0);
|
||||
assertTrue(blocked.await(5, TimeUnit.SECONDS), "Listener did not receive the first element");
|
||||
|
||||
for(int x = 1; x <= count; x++)
|
||||
{
|
||||
dispatcher.receive(x);
|
||||
}
|
||||
|
||||
release.countDown();
|
||||
return received;
|
||||
}
|
||||
|
||||
private static void awaitSize(List<Integer> list, int size) throws Exception
|
||||
{
|
||||
long timeout = System.currentTimeMillis() + 5000;
|
||||
|
||||
while(list.size() < size && System.currentTimeMillis() < timeout)
|
||||
{
|
||||
Thread.sleep(5);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: elements received while a bounded queue is full are dropped and the queued elements stay in order.
|
||||
*/
|
||||
@Test
|
||||
void boundedQueueDropsNewestElementsWhenFull() throws Exception
|
||||
{
|
||||
Dispatcher<Integer> dispatcher = new Dispatcher<>("test", 1, MAXIMUM_QUEUE_SIZE);
|
||||
List<Integer> received = dispatchWhileBlocked(dispatcher, MAXIMUM_QUEUE_SIZE + 10);
|
||||
awaitSize(received, MAXIMUM_QUEUE_SIZE + 1);
|
||||
Thread.sleep(50);
|
||||
dispatcher.stop();
|
||||
|
||||
assertEquals(List.of(0, 1, 2, 3, 4), received);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: a bounded dispatcher resumes delivery after an overflow.
|
||||
*/
|
||||
@Test
|
||||
void boundedQueueRecoversAfterOverflow() throws Exception
|
||||
{
|
||||
Dispatcher<Integer> dispatcher = new Dispatcher<>("test", 1, MAXIMUM_QUEUE_SIZE);
|
||||
List<Integer> received = dispatchWhileBlocked(dispatcher, MAXIMUM_QUEUE_SIZE + 10);
|
||||
awaitSize(received, MAXIMUM_QUEUE_SIZE + 1);
|
||||
dispatcher.receive(99);
|
||||
awaitSize(received, MAXIMUM_QUEUE_SIZE + 2);
|
||||
dispatcher.stop();
|
||||
|
||||
assertEquals(99, received.get(received.size() - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: an unbounded dispatcher doesn't drop elements.
|
||||
*/
|
||||
@Test
|
||||
void unboundedQueueKeepsAllElements() throws Exception
|
||||
{
|
||||
Dispatcher<Integer> dispatcher = new Dispatcher<>("test", 1);
|
||||
List<Integer> received = dispatchWhileBlocked(dispatcher, 100);
|
||||
awaitSize(received, 101);
|
||||
dispatcher.stop();
|
||||
|
||||
assertEquals(101, received.size());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user