From 719d370aac524836099cf093c7f96e1f135eee71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20Stenstr=C3=B6m?= Date: Wed, 18 Dec 2019 22:43:22 +0100 Subject: [PATCH 1/4] minor JavaDoc and readability improvement --- .../goxr3plus/streamplayer/stream/StreamPlayer.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/goxr3plus/streamplayer/stream/StreamPlayer.java b/src/main/java/com/goxr3plus/streamplayer/stream/StreamPlayer.java index f0ee979..3cbf3e3 100644 --- a/src/main/java/com/goxr3plus/streamplayer/stream/StreamPlayer.java +++ b/src/main/java/com/goxr3plus/streamplayer/stream/StreamPlayer.java @@ -1067,12 +1067,18 @@ public float getMinimumGain() { /** * Returns Pan precision. + *

+ * Obtains the resolution or granularity of the control, in the units that the control measures. + * The precision is the size of the increment between discrete valid values for this control, + * over the set of supported floating-point values. * - * @return The Precision Value + * @return The Precision Value for the pan control, if it exists, otherwise 0.0. */ @Override public float getPrecision() { - return !outlet.hasControl(FloatControl.Type.PAN, outlet.getPanControl()) ? 0.0F : outlet.getPanControl().getPrecision(); + return !outlet.hasControl(FloatControl.Type.PAN, outlet.getPanControl()) + ? 0 + : outlet.getPanControl().getPrecision(); } From 0ef3d2ff7dea0712edeba317de0ad6d73a85a261 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20Stenstr=C3=B6m?= Date: Wed, 18 Dec 2019 22:43:48 +0100 Subject: [PATCH 2/4] Update StreamPlayerMethodsTest.java Some unit tests are improved, but there are more to be done. --- .../stream/StreamPlayerMethodsTest.java | 96 +++++++++++++++---- 1 file changed, 75 insertions(+), 21 deletions(-) diff --git a/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerMethodsTest.java b/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerMethodsTest.java index 4cfbc5b..ba43454 100644 --- a/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerMethodsTest.java +++ b/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerMethodsTest.java @@ -4,8 +4,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import javax.sound.sampled.SourceDataLine; +import javax.sound.sampled.*; import java.io.File; +import java.io.IOException; +import java.util.List; import java.util.logging.Logger; import static org.junit.jupiter.api.Assertions.*; @@ -191,9 +193,11 @@ void totalBytes() { @Test void stopped() { - player.isStopped(); - fail("Test not done"); + assertFalse(player.isStopped()); + + player.stop(); + assertTrue(player.isStopped()); } @Test @@ -225,10 +229,17 @@ void pausedOrPlaying() { } @Test - void paused() { - player.isPaused(); + void paused() throws StreamPlayerException { + assertFalse(player.isPaused()); - fail("Test not done"); + player.open(audioFile); + assertFalse(player.isPaused()); + + player.play(); + assertFalse(player.isPaused()); + + player.pause(); + assertTrue(player.isPaused()); } @Test @@ -277,10 +288,21 @@ void play() throws StreamPlayerException { } @Test - void resume() { - player.resume(); + void resume() throws StreamPlayerException { + assertFalse(player.isPlaying()); - fail("Test not done"); + player.open(audioFile); + assertFalse(player.isPlaying()); + + player.play(); + assertTrue(player.isPlaying()); + + player.pause(); + assertFalse(player.isPlaying()); + + + player.resume(); + assertTrue(player.isPlaying()); } @Test @@ -310,9 +332,15 @@ void pan() throws StreamPlayerException { player.setPan(pan); assertEquals(pan, player.getPan(), delta); + // If we set the pan outside the permitted range, it will not change + // The permitted range is undefined. double outsideRange = 1.1; player.setPan(outsideRange); assertEquals(pan, player.getPan(), delta); + + float precision = player.getPrecision(); + assertNotEquals(0, precision); + assertEquals(3f, 1.0/precision); } @Test @@ -332,14 +360,18 @@ void open() throws StreamPlayerException { @Test void mixers() { - player.getMixers(); - - fail("Test not done"); + List mixers = player.getMixers(); + // TODO: Make this method player.getMixers() private, remove it from the interface. + // There is nothing that can be done with the information outside the private scope. } @Test void seekBytes() throws StreamPlayerException { - player.seekBytes(0); + player.open(audioFile); + int positionByte1 = player.getPositionByte(); + + player.seekBytes(100); + int positionByte2 = player.getPositionByte(); fail("Test not done"); } @@ -376,17 +408,22 @@ void positionByte() { } @Test - void precision() { - player.getPrecision(); + void precision() throws StreamPlayerException { + assertEquals(0f, player.getPrecision()); - fail("Test not done"); + player.open(audioFile); + player.play(); + + assertNotEquals(0f, player.getPrecision()); + // On one computer the precision = 1/128. There are no guarantees. } @Test - void opened() { - player.isOpened(); + void opened() throws StreamPlayerException { + assertFalse(player.isOpened()); - fail("Test not done"); + player.open(audioFile); + assertTrue(player.isOpened()); } @Test @@ -404,8 +441,25 @@ void removeStreamPlayerListener() { } @Test - void seekTo() throws StreamPlayerException { - player.seekTo(1000); + void seekTo() throws StreamPlayerException, IOException, UnsupportedAudioFileException { + + // Some tests before we do the real tests + AudioFileFormat audioFileFormat = AudioSystem.getAudioFileFormat(audioFile); + + + // Setup + player.open(audioFile); + player.play(); + player.pause(); + int positionByte1 = player.getPositionByte(); + assertNotEquals(AudioSystem.NOT_SPECIFIED, positionByte1, "If we cannot check the position, how can we verify seek?"); + + // Execute + player.seekTo(10); + + // Verify + int positionByte2 = player.getPositionByte(); + assertNotEquals(positionByte2, positionByte1); fail("Test not done"); } From 68c67ca20e6d025a89d4dbc02931331e126c6c48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20Stenstr=C3=B6m?= Date: Fri, 20 Dec 2019 23:17:22 +0100 Subject: [PATCH 3/4] Improved and re-structured unit tests --- .../StreamPlayerFutureImprovementTest.java | 47 +++++ .../stream/StreamPlayerMethodsTest.java | 166 +++++++++++++----- 2 files changed, 172 insertions(+), 41 deletions(-) create mode 100644 src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerFutureImprovementTest.java diff --git a/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerFutureImprovementTest.java b/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerFutureImprovementTest.java new file mode 100644 index 0000000..807c4d9 --- /dev/null +++ b/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerFutureImprovementTest.java @@ -0,0 +1,47 @@ +package com.goxr3plus.streamplayer.stream; + +import com.goxr3plus.streamplayer.enums.Status; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javax.sound.sampled.AudioFileFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.SourceDataLine; +import javax.sound.sampled.UnsupportedAudioFileException; +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; + +/** + * Tests of all or most of the public methods of StreamPlayer. + * These unit tests are written primarily as documentation of the behavior and as example use case, + * not as a part of test driven development. + */ +public class StreamPlayerFutureImprovementTest { + StreamPlayer player; + private File audioFile; + + @BeforeEach + void setup() { + final Logger logger = mock(Logger.class); + player = new StreamPlayer(logger); + audioFile = new File("Logic - Ballin [Bass Boosted].mp3"); + } + + /** + * This test fails if it's permitted to add a null to the StreamPlayer listener list. + */ + @Test + void addStreamPlayerListener_dontAcceptNull() { + // Currently, we can add a null to the list of stream player listeners. + // Should that really be allowed? + assertThrows(Exception.class, () -> player.addStreamPlayerListener(null)); + + fail("Test not done"); + } + +} diff --git a/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerMethodsTest.java b/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerMethodsTest.java index ba43454..98dcab0 100644 --- a/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerMethodsTest.java +++ b/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerMethodsTest.java @@ -3,15 +3,18 @@ import com.goxr3plus.streamplayer.enums.Status; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import javax.sound.sampled.*; import java.io.File; import java.io.IOException; import java.util.List; +import java.util.Map; import java.util.logging.Logger; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.*; /** * Tests of all or most of the public methods of StreamPlayer. @@ -87,7 +90,7 @@ void status() throws StreamPlayerException { @Test void gain() throws StreamPlayerException, InterruptedException { // Setup - final double gain1 = 0.99; + final double gain1_dB = 0.5; final double gain2 = 0.2; final double delta = 0.05; final boolean listen = false; @@ -97,8 +100,8 @@ void gain() throws StreamPlayerException, InterruptedException { player.open(audioFile); player.seekTo(30); player.play(); - player.setGain(gain1); - final float actualGain1First = player.getGainValue(); + player.setGain(gain1_dB); + final float actualGain0 = player.getGainValue(); if (listen) Thread.sleep(2000); final float actualGain1 = player.getGainValue(); @@ -106,16 +109,19 @@ void gain() throws StreamPlayerException, InterruptedException { if (listen) Thread.sleep(2000); final float actualGain2 = player.getGainValue(); - player.setGain(gain1); + player.setGain(gain1_dB); if (listen) Thread.sleep(2000); player.stop(); // Verify assertEquals(0, initialGain); - assertEquals(actualGain1First, actualGain1); - assertEquals(gain1, actualGain1, delta); // TODO: Investigate probable bug. - // fail("Test not done"); + assertEquals(actualGain0, actualGain1); + assertEquals(20.0 * Math.log10(gain1_dB), actualGain1, delta); + + // TODO: Consider changing the API. setGain() and getGainValue() have different scales. + // setGain(linear scale), + // whereas getGainValue() returns a logarithmic dB scale value. This is inconsistent. } /** @@ -185,10 +191,17 @@ void maximumGain() throws StreamPlayerException { } @Test - void totalBytes() { - player.getTotalBytes(); + void totalBytes() throws StreamPlayerException, InterruptedException { + int expectedLengthOfExampleAudioFile = 5877062; - fail("Test not done"); + + assertEquals(-1, player.getTotalBytes()); + + player.open(audioFile); + assertEquals(expectedLengthOfExampleAudioFile, player.getTotalBytes()); + + player.play(); + assertEquals(expectedLengthOfExampleAudioFile, player.getTotalBytes()); } @Test @@ -213,19 +226,36 @@ void sourceDataLine() throws StreamPlayerException { } @Test - void playing() { - final boolean playing = player.isPlaying(); + void playing() throws StreamPlayerException { + + assertFalse(player.isPlaying()); - assertFalse(playing); + player.open(audioFile); + assertFalse(player.isPlaying()); - fail("Test not done"); + player.play(); + assertTrue(player.isPlaying()); + + player.pause(); + assertFalse(player.isPlaying()); } @Test - void pausedOrPlaying() { - player.isPausedOrPlaying(); + void pausedOrPlaying() throws StreamPlayerException { - fail("Test not done"); + assertFalse(player.isPausedOrPlaying()); + + player.open(audioFile); + assertFalse(player.isPausedOrPlaying()); + + player.play(); + assertTrue(player.isPausedOrPlaying()); + + player.pause(); + assertTrue(player.isPausedOrPlaying()); + + player.stop(); + assertFalse(player.isPausedOrPlaying()); } @Test @@ -243,34 +273,76 @@ void paused() throws StreamPlayerException { } @Test - void addStreamPlayerListener_dontAcceptNull() { - assertThrows(Exception.class, () -> player.addStreamPlayerListener(null)); + void addStreamPlayerListener() throws StreamPlayerException, InterruptedException { + // Setup + final StreamPlayerListener listener = mock(StreamPlayerListener.class); - fail("Test not done"); - } + ArgumentCaptor dataSourceCaptor = ArgumentCaptor.forClass(Object.class); + ArgumentCaptor propertiesCaptor1 = ArgumentCaptor.forClass(Map.class); - @Test - void addStreamPlayerListener() { - final StreamPlayerListener listener = mock(StreamPlayerListener.class); + // Execute player.addStreamPlayerListener(listener); + player.open(audioFile); + player.play(); + Thread.sleep(30); + + // Verify + verify(listener).opened(dataSourceCaptor.capture(), propertiesCaptor1.capture()); + Object value = dataSourceCaptor.getValue(); + assertTrue(value instanceof File); + + Map value11 = propertiesCaptor1.getValue(); + + assertTrue(value11.containsKey("basicplayer.sourcedataline")); + + verify(listener, times(4)).statusUpdated(any()); + + verify(listener, times(1)).opened(any(), any()); + + verify(listener, atLeast(5)).progress(anyInt(), anyLong(), any(), any()); + verify(listener, atMost(10)).progress(anyInt(), anyLong(), any(), any()); + + // TODO: Make separate tests for the different calls made to the listener + // TODO: Do we need to test the values passed to these methods? - fail("Test not done"); // TODO: CHeck that the listener is actually added } @Test - void mute() { - player.getMute(); + void mute() throws StreamPlayerException { + // TODO: How can mute be tested, without too much assumptions about the actual implementation? + // A manual test would involve listening. + + + assertFalse(player.getMute()); + player.open(audioFile); + player.play(); + player.setMute(true); + assertTrue(player.getMute()); player.setMute(false); + assertFalse(player.getMute()); - fail("Test not done"); } @Test - void speedFactor() { - player.getSpeedFactor(); - player.setSpeedFactor(1000); + void speedFactor() throws StreamPlayerException, InterruptedException { + assertEquals(player.getSpeedFactor(), 1); + + double fast = 1; + player.setSpeedFactor(fast); + assertEquals(fast, player.getSpeedFactor()); + + double slow = 0.5; + player.open(audioFile); + player.play(); + player.setSpeedFactor(slow); + Thread.sleep(50); + assertEquals(slow, player.getSpeedFactor()); + + // TODO: Find a way to verify that the speed factor actually works. That it can be read back is no proof. + // I might be possible to play a short sequence of known length, and measure the time it takes. + // But things that take time are generally not advisable in unit tests. + - fail("Test not done"); } @Test @@ -387,10 +459,18 @@ void lineBufferSize() { } @Test - void lineCurrentBufferSize() { - player.getLineCurrentBufferSize(); + void lineCurrentBufferSize() throws StreamPlayerException { + // TODO: Document the purpose of getLineCurrentBufferSize(). What is it good for? + // Can it be removed? The method doesn't really return the current line buffer size, + // but a cached value, which might be the same thing. Hard to say. - fail("Test not done"); + assertEquals(-1, player.getLineCurrentBufferSize(), "Initially, the buffer size is undefined, coded as -1."); + + player.open(audioFile); + assertEquals(-1, player.getLineCurrentBufferSize(), "After the player is opened, the buffer size is undefined"); + + player.play(); + assertEquals(2 * 44100, player.getLineCurrentBufferSize(), "After the play starts, the buffer size 1 second at CD sampling rate"); } @Test @@ -451,17 +531,21 @@ void seekTo() throws StreamPlayerException, IOException, UnsupportedAudioFileExc player.open(audioFile); player.play(); player.pause(); - int positionByte1 = player.getPositionByte(); - assertNotEquals(AudioSystem.NOT_SPECIFIED, positionByte1, "If we cannot check the position, how can we verify seek?"); + int encodedStreamPosition1 = player.getEncodedStreamPosition(); // Execute player.seekTo(10); // Verify - int positionByte2 = player.getPositionByte(); - assertNotEquals(positionByte2, positionByte1); + int encodedStreamPosition2 = player.getEncodedStreamPosition(); + assertTrue(encodedStreamPosition2 > encodedStreamPosition1); - fail("Test not done"); + // Execute: go backwards + player.seekTo(5); + + // Verify: position goes backwards + int encodedStreamPosition3 = player.getEncodedStreamPosition(); + assertTrue(encodedStreamPosition3 < encodedStreamPosition2); } @Test From dca8c7a8e3ae6c1e4136358b810df3377136c526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20Stenstr=C3=B6m?= Date: Sun, 22 Dec 2019 11:09:10 +0100 Subject: [PATCH 4/4] Improved and more complete unit tests. StreamPlayerMethodsTest still contains tests that don't pass. The tested methods are candidates for removal from StreamPlayer. All tests in StreamPlayerMethodsTest must be reviewed: Do they actually verify anything that need to be verified? Or are they too coupled with the current implementation? StreamPlayerFutureImprovementTest contains tests that currently fail. Failures are caused by behavior in StreamPlayer which I think is wrong or bad. But I may have misinterpreted the intended behavior. --- .../StreamPlayerFutureImprovementTest.java | 26 +++++++ .../stream/StreamPlayerMethodsTest.java | 71 ++++++++++++------- 2 files changed, 73 insertions(+), 24 deletions(-) diff --git a/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerFutureImprovementTest.java b/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerFutureImprovementTest.java index 807c4d9..4a6c452 100644 --- a/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerFutureImprovementTest.java +++ b/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerFutureImprovementTest.java @@ -2,6 +2,7 @@ import com.goxr3plus.streamplayer.enums.Status; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import javax.sound.sampled.AudioFileFormat; @@ -44,4 +45,29 @@ void addStreamPlayerListener_dontAcceptNull() { fail("Test not done"); } + + @Test + @DisplayName("When play() is called without first calling open(), an exception is thrown") + void playingUnopenedSourceThrowsException() { + + assertThrows(Exception.class, () -> player.play()); + } + + @Test + void seekBytes() throws StreamPlayerException { + player.open(audioFile); + player.play(); + int positionByte1 = player.getPositionByte(); + + player.seekBytes(100); + int positionByte2 = player.getPositionByte(); + + assertTrue( positionByte2 > positionByte1); + + // TODO: It seems that getPositionByte doesn't work. + // It isn't called from within this project, except for in this test. + // It is however called by XR3Player. If XR3Player needs this method, it must be tested + // within this project. The method relies on a map, which doesn't seem to be updated by play() + } + } diff --git a/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerMethodsTest.java b/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerMethodsTest.java index 98dcab0..1c247e4 100644 --- a/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerMethodsTest.java +++ b/src/test/java/com/goxr3plus/streamplayer/stream/StreamPlayerMethodsTest.java @@ -135,7 +135,7 @@ void gain() throws StreamPlayerException, InterruptedException { @Test void logScaleGain() throws StreamPlayerException, InterruptedException { // Setup - final boolean listen = true; + final boolean listen = false; // Set to true to listen to the test. // Exercise @@ -299,8 +299,8 @@ void addStreamPlayerListener() throws StreamPlayerException, InterruptedExceptio verify(listener, times(1)).opened(any(), any()); - verify(listener, atLeast(5)).progress(anyInt(), anyLong(), any(), any()); - verify(listener, atMost(10)).progress(anyInt(), anyLong(), any(), any()); + verify(listener, atLeast(4)).progress(anyInt(), anyLong(), any(), any()); + verify(listener, atMost(30)).progress(anyInt(), anyLong(), any(), any()); // TODO: Make separate tests for the different calls made to the listener // TODO: Do we need to test the values passed to these methods? @@ -348,15 +348,25 @@ void speedFactor() throws StreamPlayerException, InterruptedException { @Test void equalizer() { player.setEqualizer(null, 0); - - fail("Test not done"); + // TODO: Find out what the intention of setEqualizer() is, and make a test for that assumption. } @Test - void play() throws StreamPlayerException { + void play() throws StreamPlayerException, InterruptedException { + // Setup + player.open(audioFile); + + // Pre-validate + assertFalse(player.isPlaying()); + + // Execute player.play(); - fail("Test not done"); + // Verify + assertTrue(player.isPlaying()); + + // TODO: Find way to verify that the player is actually playing, that doesn't need listening. + // The method might look at the playing position, but it must be fairly quick. } @Test @@ -378,17 +388,33 @@ void resume() throws StreamPlayerException { } @Test - void pause() { + void pause() throws StreamPlayerException { + + // Setup + player.open(audioFile); + player.play(); + // Pre-validate + assertFalse(player.isPaused()); + + // Execute player.pause(); - fail("Test not done"); + // Verify + assertTrue(player.isPaused()); + } @Test void stop() { + + assertFalse(player.isStopped()); + player.stop(); - fail("Test not done"); + assertTrue(player.isStopped()); + + // TODO: Find a way to verify that playback is stopped by running the stop method. + // The isStopped() method is not enough. } @Test @@ -412,22 +438,27 @@ void pan() throws StreamPlayerException { float precision = player.getPrecision(); assertNotEquals(0, precision); - assertEquals(3f, 1.0/precision); + double expected = 128.0; // Possibly platform dependent. Tested on a Mac with Intellij. + assertEquals(expected, 1.0/precision, 2.0); } @Test void unknown() { player.isUnknown(); - - fail("Test not done"); + // This is a useless test of a useless method. + // TODO: Remove player.isUnknown(). It's not used, and it's useless. + // There is already getStatus(). } @Test void open() throws StreamPlayerException { - File file = null; + File file = spy(audioFile); player.open(file); + verify(file, atLeast(1)).getPath(); - fail("Test not done"); + // It's unclear what the contract of open() is; what we need it to do. + // It's a pre-requisite for play(), but play() doesn't throw an + // exception if open() is missing. } @Test @@ -437,19 +468,11 @@ void mixers() { // There is nothing that can be done with the information outside the private scope. } - @Test - void seekBytes() throws StreamPlayerException { - player.open(audioFile); - int positionByte1 = player.getPositionByte(); - - player.seekBytes(100); - int positionByte2 = player.getPositionByte(); - fail("Test not done"); - } // The methods tested below aren't used elsewhere in this project, nor in XR3Player + // TODO: Consider each of the tested methods below, to see if they can be removed from StreamPlayer. @Test void lineBufferSize() {