JeffGrigg

Untitled

May 13th, 2023
359
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Java 5 38.44 KB | None | 0 0
  1. /*
  2. GuessTheNumber.java = simple “guess the number” game
  3.  
  4. tests/GuessTheNumberTest.java = JUnit test for the game
  5. tests/SystemInputOutputTesterTest.java = Test the main JUnit testing factory.
  6. tests/RandomSourceTest.java = Test the "Random" class related functionality and mocks used by the "GuessTheNumber" class.
  7. tests/ExpectedException.java = Independent exception (child of Error) to throw when the "code under test" was expected to throw an exception (typically an assertion error), but did not.
  8.  
  9. library/RandomSource.java = creates "Random" instances (or mock) for the Program Under Test
  10. library/RandomCreatorInterface.java = interface to create "Random" instances (enables lambda expressions)
  11. library/AbstractMockRandomSwapper.java = swaps mock Random subclasses in on construction and out on close
  12. library/MockRandomSingleValue.java = mock implementation for single-use random number
  13.  
  14. library/SystemInputOutputTester.java = Builder for console I/O testing
  15. library/CallbackThrowsException.java = interface for call back to "main" method -- to handle checked exceptions
  16. library/MockStandardOutputStream.java = Capture Standard Output (or Error)
  17. library/MockInputStream.java = Provide input when needed. Calls back to SystemInputOutputTester to validate intervening output and to provide the input data.
  18. library/ExpectedInputOutputStepBase.java = common base class for input/output expectations
  19. library/ExpectedTextOutputStep.java = Expect that the given text output is written to Standard Output (or Error).
  20. library/ProvideLineInputStep.java = Provide line of Console Input at that point in the expectations list.
  21.  
  22. */
  23.  
  24. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  25. //
  26. // File Name:   GuessTheNumber.java
  27. //
  28. package p20230510_TestConsoleIO;
  29.  
  30. import p20230510_TestConsoleIO.library.RandomSource;
  31.  
  32. import java.util.Scanner;
  33.  
  34. public class GuessTheNumber {
  35.  
  36.     public static void main(final String[] args) {
  37.         System.out.println("Welcome to the number guessing program.");
  38.         System.out.println("I am thinking of a number in the range of 1 to 100, inclusive.");
  39.         System.out.println("You need to guess this number, with a minimum number of guesses.");
  40.         System.out.println("Each time you guess, I will tell you if my number is HIGHER or LOWER than your guess.");
  41.  
  42.         final var random = RandomSource.create();
  43.         final int numberToGuess = random.nextInt(100) + 1;
  44.         final var scanner = new Scanner(System.in);
  45.         var numberOfGuesses = 0;
  46.  
  47.         while (true) {
  48.             System.out.println("What is your guess?");
  49.             final var userGuess = scanner.nextInt();
  50.             ++numberOfGuesses;
  51.             if (userGuess < numberToGuess) {
  52.                 System.out.println("Your guess of " + userGuess + " is TOO LOW.");
  53.             } else if (userGuess > numberToGuess) {
  54.                 System.out.println("Your guess of " + userGuess + " is TOO HIGH.");
  55.             } else {
  56.                 System.out.println("Your guess of " + userGuess + " is CORRECT in "
  57.                         + (numberOfGuesses == 1 ? "one guess!!!" : (numberOfGuesses + " guesses!")));
  58.                 break;
  59.             }
  60.         }
  61.     }
  62.  
  63. }
  64.  
  65. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  66. //
  67. // File Name:   tests/GuessTheNumberTest.java
  68. //
  69. package p20230510_TestConsoleIO.tests;
  70.  
  71. //  https://mastodon.social/@GeePawHill/110341886456025504 = Hard to TDD "Hello World" and simple console I/O "games"
  72. //  https://mastodon.social/@JeffGrigg/110347321176183414 = Jeff "Grigg says that "doing TDD through a User Interface is Always Difficult."
  73. //  https://mastodon.social/@GeePawHill/110351924530710192 = GeePawHill proposes hiding complexity in a library to protect students
  74. //  https://mastodon.social/@JeffGrigg/110356052836313274 = posting my implementation
  75. //  https://mastodon.social/@JeffGrigg/110358610496829363 = my implementation, cleaned and improved -- "Cleaned it up a bit. Better interfaces and reduced coupling. Separate "library" and "tests" packages. Added tests."
  76.  
  77. import org.junit.Test;
  78. import p20230510_TestConsoleIO.GuessTheNumber;
  79. import p20230510_TestConsoleIO.library.MockRandomSingleValue;
  80. import p20230510_TestConsoleIO.library.SystemInputOutputTester;
  81.  
  82. import java.io.IOException;
  83.  
  84. public class GuessTheNumberTest {
  85.  
  86.     @Test
  87.     public void testHighsAndLowsTo33() throws IOException {
  88.  
  89.         final var systemNumberWeAreTryingToGuess = 33;
  90.  
  91.         try (final var mockRandomNumberGenerator = new MockRandomSingleValue(systemNumberWeAreTryingToGuess)) {
  92.             SystemInputOutputTester
  93.                     .whenCallingThisMethod(() -> {
  94.                         GuessTheNumber.main(null);
  95.                     })
  96.                     .assertTextOutput("Welcome to the number guessing program.")
  97.                     //
  98.                     .assertTextOutput("I am thinking of a number in the range of 1 to 100, inclusive.")
  99.                     .assertTextOutput("You need to guess this number, with a minimum number of guesses.")
  100.                     .assertTextOutput("Each time you guess, I will tell you if my number is HIGHER or LOWER than your guess.")
  101.                     //
  102.                     .assertTextOutput("What is your guess?")
  103.                     .provideLineInput("50")
  104.                     .assertTextOutput("Your guess of 50 is TOO HIGH.")
  105.                     //
  106.                     .assertTextOutput("What is your guess?")
  107.                     .provideLineInput("25")
  108.                     .assertTextOutput("Your guess of 25 is TOO LOW.")
  109.                     //
  110.                     .assertTextOutput("What is your guess?")
  111.                     .provideLineInput("37")
  112.                     .assertTextOutput("Your guess of 37 is TOO HIGH.")
  113.                     //
  114.                     .assertTextOutput("What is your guess?")
  115.                     .provideLineInput("31")
  116.                     .assertTextOutput("Your guess of 31 is TOO LOW.")
  117.                     //
  118.                     .assertTextOutput("What is your guess?")
  119.                     .provideLineInput("34")
  120.                     .assertTextOutput("Your guess of 34 is TOO HIGH.")
  121.                     //
  122.                     .assertTextOutput("What is your guess?")
  123.                     .provideLineInput("32")
  124.                     .assertTextOutput("Your guess of 32 is TOO LOW.")
  125.                     //
  126.                     .assertTextOutput("What is your guess?")
  127.                     .provideLineInput("33")
  128.                     .assertTextOutput("Your guess of 33 is CORRECT in 7 guesses!")
  129.                     //
  130.                     .assertThatTheMethodReturnsHere();
  131.         }
  132.     }
  133.  
  134.     @Test
  135.     public void testMaximumGuesses() throws IOException {
  136.  
  137.         final var systemNumberWeAreTryingToGuess = 100;
  138.  
  139.         try (final var mockRandomNumberGenerator = new MockRandomSingleValue(systemNumberWeAreTryingToGuess)) {
  140.             SystemInputOutputTester
  141.                     .whenCallingThisMethod(() -> {
  142.                         GuessTheNumber.main(null);
  143.                     })
  144.                     .assertTextOutput("Welcome to the number guessing program.")
  145.                     //
  146.                     .assertTextOutput("I am thinking of a number in the range of 1 to 100, inclusive.")
  147.                     .assertTextOutput("You need to guess this number, with a minimum number of guesses.")
  148.                     .assertTextOutput("Each time you guess, I will tell you if my number is HIGHER or LOWER than your guess.")
  149.                     //
  150.                     .assertTextOutput("What is your guess?")
  151.                     .provideLineInput("50")
  152.                     .assertTextOutput("Your guess of 50 is TOO LOW.")
  153.                     //
  154.                     .assertTextOutput("What is your guess?")
  155.                     .provideLineInput("75")
  156.                     .assertTextOutput("Your guess of 75 is TOO LOW.")
  157.                     //
  158.                     .assertTextOutput("What is your guess?")
  159.                     .provideLineInput("88")
  160.                     .assertTextOutput("Your guess of 88 is TOO LOW.")
  161.                     //
  162.                     .assertTextOutput("What is your guess?")
  163.                     .provideLineInput("94")
  164.                     .assertTextOutput("Your guess of 94 is TOO LOW.")
  165.                     //
  166.                     .assertTextOutput("What is your guess?")
  167.                     .provideLineInput("97")
  168.                     .assertTextOutput("Your guess of 97 is TOO LOW.")
  169.                     //
  170.                     .assertTextOutput("What is your guess?")
  171.                     .provideLineInput("99")
  172.                     .assertTextOutput("Your guess of 99 is TOO LOW.")
  173.                     //
  174.                     .assertTextOutput("What is your guess?")
  175.                     .provideLineInput("100")
  176.                     .assertTextOutput("Your guess of 100 is CORRECT in 7 guesses!")
  177.                     //
  178.                     .assertThatTheMethodReturnsHere();
  179.         }
  180.     }
  181.  
  182.     @Test
  183.     public void testFirstGuessIsCorrect() throws IOException {
  184.  
  185.         final var systemNumberWeAreTryingToGuess = 24;
  186.  
  187.         try (final var mockRandomNumberGenerator = new MockRandomSingleValue(systemNumberWeAreTryingToGuess)) {
  188.             SystemInputOutputTester
  189.                     .whenCallingThisMethod(() -> {
  190.                         GuessTheNumber.main(null);
  191.                     })
  192.                     .assertTextOutput("Welcome to the number guessing program.")
  193.                     //
  194.                     .assertTextOutput("I am thinking of a number in the range of 1 to 100, inclusive.")
  195.                     .assertTextOutput("You need to guess this number, with a minimum number of guesses.")
  196.                     .assertTextOutput("Each time you guess, I will tell you if my number is HIGHER or LOWER than your guess.")
  197.                     //
  198.                     .assertTextOutput("What is your guess?")
  199.                     .provideLineInput("24")
  200.                     .assertTextOutput("Your guess of 24 is CORRECT in one guess!!!")
  201.                     //
  202.                     .assertThatTheMethodReturnsHere();
  203.         }
  204.     }
  205.  
  206.     @Test
  207.     public void testSecondGuessIsCorrect() throws IOException {
  208.  
  209.         final var systemNumberWeAreTryingToGuess = 33;
  210.  
  211.         try (final var mockRandomNumberGenerator = new MockRandomSingleValue(systemNumberWeAreTryingToGuess)) {
  212.             SystemInputOutputTester
  213.                     .whenCallingThisMethod(() -> {
  214.                         GuessTheNumber.main(null);
  215.                     })
  216.                     .assertTextOutput("Welcome to the number guessing program.")
  217.                     //
  218.                     .assertTextOutput("I am thinking of a number in the range of 1 to 100, inclusive.")
  219.                     .assertTextOutput("You need to guess this number, with a minimum number of guesses.")
  220.                     .assertTextOutput("Each time you guess, I will tell you if my number is HIGHER or LOWER than your guess.")
  221.                     //
  222.                     .assertTextOutput("What is your guess?")
  223.                     .provideLineInput("86")
  224.                     .assertTextOutput("Your guess of 86 is TOO HIGH.")
  225.                     //
  226.                     .assertTextOutput("What is your guess?")
  227.                     .provideLineInput("33")
  228.                     .assertTextOutput("Your guess of 33 is CORRECT in 2 guesses!")
  229.                     //
  230.                     .assertThatTheMethodReturnsHere();
  231.         }
  232.     }
  233.  
  234. }
  235.  
  236. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  237. //
  238. // File Name:   tests/SystemInputOutputTesterTest.java
  239. //
  240. package p20230510_TestConsoleIO.tests;
  241.  
  242. import junit.framework.AssertionFailedError;
  243. import org.junit.Test;
  244. import p20230510_TestConsoleIO.library.SystemInputOutputTester;
  245.  
  246. import java.io.BufferedReader;
  247. import java.io.IOException;
  248. import java.io.InputStreamReader;
  249.  
  250. import static org.junit.Assert.assertEquals;
  251.  
  252. public class SystemInputOutputTesterTest {
  253.  
  254.     private static class DoNothingProgram {
  255.         public static void main(String[] args) {
  256.             // Intentionally do NOTHING here.
  257.         }
  258.     }
  259.  
  260.     @Test
  261.     public void testDoNothingProgram() {
  262.         SystemInputOutputTester
  263.                 .whenCallingThisMethod(() -> {
  264.                     DoNothingProgram.main(null);
  265.                 })
  266.                 .assertThatTheMethodReturnsHere();
  267.     }
  268.  
  269.     @Test
  270.     public void testMissingMessage() {
  271.         try {
  272.             SystemInputOutputTester
  273.                     .whenCallingThisMethod(() -> {
  274.                         DoNothingProgram.main(null);
  275.                     })
  276.                     .assertTextOutput("This program does not and should not produce this output.")
  277.                     .assertThatTheMethodReturnsHere();
  278.             throw new ExpectedException("Expected AssertionFailedError to be thrown by the code above.");
  279.         } catch (final AssertionFailedError ex) {
  280.             assertEquals("Failed to find <This program does not and should not produce this output.> in <>", ex.getMessage());
  281.         }
  282.     }
  283.  
  284.     @Test
  285.     public void testProgramTerminatesWithMoreInputToProcess() {
  286.         try {
  287.             SystemInputOutputTester
  288.                     .whenCallingThisMethod(() -> {
  289.                         DoNothingProgram.main(null);
  290.                     })
  291.                     .provideLineInput("Unused Input Line")
  292.                     .assertThatTheMethodReturnsHere();
  293.             throw new ExpectedException("Expected AssertionFailedError to be thrown by the code above.");
  294.         } catch (final AssertionFailedError ex) {
  295.             assertEquals("Program has exited, but the tests say that we should provide further input."
  296.                     + System.lineSeparator() + "  ProvideLineInputStep('Unused Input Line')", ex.getMessage());
  297.         }
  298.     }
  299.  
  300.     @Test
  301.     public void testHelloWorldProgram() {
  302.         SystemInputOutputTester
  303.                 .whenCallingThisMethod(() -> {
  304.                     HelloWorldProgram.main(null);
  305.                 })
  306.                 .assertTextOutput("Hello world!")
  307.                 .assertThatTheMethodReturnsHere();
  308.     }
  309.  
  310.     private static class HelloWorldProgram {
  311.         public static void main(String[] args) {
  312.             System.out.println("Hello world!");
  313.         }
  314.     }
  315.  
  316.     @Test
  317.     public void testHelloNameProgram() {
  318.         SystemInputOutputTester
  319.                 .whenCallingThisMethod(() -> {
  320.                     HelloNameProgram.main(null);
  321.                 })
  322.                 .assertTextOutput("What is your name? ")
  323.                 .provideLineInput("John")
  324.                 .assertTextOutput("Hello John!")
  325.                 .assertThatTheMethodReturnsHere();
  326.     }
  327.  
  328.     private static class HelloNameProgram {
  329.         public static void main(String[] args) throws IOException {
  330.             System.out.println("What is your name? ");
  331.             final var in = new BufferedReader(new InputStreamReader(System.in));
  332.             final var name = in.readLine();
  333.             System.out.println("Hello " + name + "!");
  334.         }
  335.     }
  336.  
  337. }
  338.  
  339. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  340. //
  341. // File Name:   tests/RandomSourceTest.java
  342. //
  343. package p20230510_TestConsoleIO.tests;
  344.  
  345. import org.junit.Test;
  346. import p20230510_TestConsoleIO.library.MockRandomSingleValue;
  347. import p20230510_TestConsoleIO.library.RandomSource;
  348.  
  349. import java.io.IOException;
  350. import java.rmi.server.ExportException;
  351. import java.util.Random;
  352. import java.util.TreeSet;
  353.  
  354. import static org.junit.Assert.*;
  355.  
  356. public class RandomSourceTest {
  357.  
  358.     private static final double ODDS_OF_FALSE_TEST_FAILURE = 0.000001;   // -- PER number we're looking for.
  359.     private static final String NL = System.lineSeparator();
  360.  
  361.     private final Random _random = RandomSource.create();
  362.     private double _lowestConfidence = 1.0;
  363.  
  364.     @Test
  365.     public void testCreateRandomNumberGenerator() throws IOException {
  366.  
  367.         // Initial (production) state:
  368.         assertSame("Initial Java Random number generator class;", Random.class, _random.getClass());
  369.  
  370.         // Creating mock Random number generator swaps in a factory for it as a side-effect:
  371.         try (final var mockRandomNumberGenerator = new MockRandomSingleValue(13)) {
  372.             assertSame(mockRandomNumberGenerator, RandomSource.create());
  373.             assertEquals("Mock Random number, as configured above;",
  374.                     13, mockRandomNumberGenerator.nextInt(100) + 1);
  375.             try {
  376.                 mockRandomNumberGenerator.nextInt(100);
  377.                 throw new ExportException("Expected AssertionError when MockRandomSingleValue instance is used more than once.");
  378.             } catch (final AssertionError ex) {
  379.                 assertEquals("'Random' value must be in the range of 0 <= value < bound (of 100).  The value -1 is outside this range in class " + MockRandomSingleValue.class.getName() + ".",
  380.                         ex.getMessage());
  381.             }
  382.         }
  383.         // Restored to initial (production) state by ".close()" call at the end of the "try (with resource)" block, above.
  384.         assertSame("Initial Random generator restored on 'close();", Random.class, _random.getClass());
  385.     }
  386.  
  387.     @Test
  388.     public void testD1() {
  389.         assertEquals("1st;", 0, _random.nextInt(1));
  390.         assertEquals("2nd;", 0, _random.nextInt(1));
  391.         assertEquals("3rd;", 0, _random.nextInt(1));
  392.         assertEquals("4th;", 0, _random.nextInt(1));
  393.         assertEquals("5th;", 0, _random.nextInt(1));
  394.     }
  395.  
  396.     @Test
  397.     public void test2toN() {
  398.  
  399.         _lowestConfidence = 1.0;
  400.  
  401.         var increment = 1;
  402.         for (var bound = 2; bound <= 200; bound += increment) {
  403.             if (bound >= 10) {
  404.                 increment = 5;
  405.             }
  406.  
  407.             final var firstAndLastToFind = new TreeSet<Integer>();
  408.             firstAndLastToFind.add(0);
  409.             firstAndLastToFind.add(bound - 1);
  410.  
  411.             verifyThatBoundryValueAreFound(bound, firstAndLastToFind);
  412.         }
  413.  
  414.         System.out.println("Lowest Confidence Level = " + _lowestConfidence);
  415.     }
  416.  
  417.     @Test
  418.     public void testNegativeOneIsOutOfBounds() {
  419.  
  420.         final var bound = 5;
  421.         final var firstAndLastToFind = new TreeSet<Integer>();
  422.         firstAndLastToFind.add(-1);
  423.  
  424.         try {
  425.             verifyThatBoundryValueAreFound(bound, firstAndLastToFind);
  426.  
  427.             throw new ExpectedException("Expected AssertionFailedError to be thrown by the code above.");
  428.         } catch (final AssertionError ex) {
  429.             final var exceptionMessage = ex.getMessage();
  430.             final var expectedMessagePrefix = "Unable to find value(s) [-1] after ";
  431.             final var expectedMessageSuffix = " tries." + NL
  432.                     + "  Values found = [0, 1, 2, 3, 4]" + NL
  433.                     + "  Giving up because the count of unique values found (5) is (at least) equal to the bound value of 5.";
  434.             assertTrue("Expected message" + NL
  435.                             + " <" + exceptionMessage + ">" + NL
  436.                             + " to start with" + NL
  437.                             + " <" + expectedMessagePrefix + ">.",
  438.                     exceptionMessage.startsWith(expectedMessagePrefix));
  439.             assertTrue("Expected message" + NL
  440.                             + " <" + exceptionMessage + ">" + NL
  441.                             + " to end with" + NL
  442.                             + " <" + expectedMessageSuffix + ">.",
  443.                     exceptionMessage.endsWith(expectedMessageSuffix));
  444.         }
  445.     }
  446.  
  447.     @Test
  448.     public void testValueEqualsBoundNeverHappens() {
  449.  
  450.         final var bound = 20;
  451.         final var firstAndLastToFind = new TreeSet<Integer>();
  452.         firstAndLastToFind.add(bound);
  453.  
  454.         try {
  455.             verifyThatBoundryValueAreFound(bound, firstAndLastToFind);
  456.  
  457.             throw new ExpectedException("Expected AssertionFailedError to be thrown by the code above.");
  458.         } catch (final AssertionError ex) {
  459.             final var exceptionMessage = ex.getMessage();
  460.             final var expectedMessagePrefix = "Unable to find value(s) [20] after ";
  461.             final var expectedMessageSuffix = " tries." + NL
  462.                     + "  Values found = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]" + NL
  463.                     + "  Giving up because the count of unique values found (20) is (at least) equal to the bound value of 20.";
  464.             assertTrue("Expected message" + NL
  465.                             + " <" + exceptionMessage + ">" + NL
  466.                             + " to start with" + NL
  467.                             + " <" + expectedMessagePrefix + ">.",
  468.                     exceptionMessage.startsWith(expectedMessagePrefix));
  469.             assertTrue("Expected message" + NL
  470.                             + " <" + exceptionMessage + ">" + NL
  471.                             + " to end with" + NL
  472.                             + " <" + expectedMessageSuffix + ">.",
  473.                     exceptionMessage.endsWith(expectedMessageSuffix));
  474.         }
  475.     }
  476.  
  477.     private void verifyThatBoundryValueAreFound(final int bound, final TreeSet<Integer> firstAndLastToFind) {
  478.         final var valuesFound = new TreeSet<Integer>();
  479.  
  480.         int numberOfTries = 0;
  481.         double oddsOfGettingThisFarWithNoMatch = 1.0;   // (100% = always gets here!)
  482.  
  483.         while (oddsOfGettingThisFarWithNoMatch > ODDS_OF_FALSE_TEST_FAILURE) {
  484.  
  485.             final var randomValue = _random.nextInt(bound);
  486.  
  487.             ++numberOfTries;
  488.             final double oddsOfNotGettingAMatchThisTime = ((double) (bound - firstAndLastToFind.size())) / bound;
  489.             oddsOfGettingThisFarWithNoMatch *= oddsOfNotGettingAMatchThisTime;
  490.  
  491.             if (oddsOfGettingThisFarWithNoMatch > 0.0 && oddsOfGettingThisFarWithNoMatch < _lowestConfidence) {
  492.                 _lowestConfidence = oddsOfGettingThisFarWithNoMatch;
  493.             }
  494.  
  495.             final var isAUniqueNewNumber = valuesFound.add(randomValue);
  496.             if (firstAndLastToFind.remove(randomValue)) {   // true if we did find and remove a value
  497.                 if (firstAndLastToFind.isEmpty()) {
  498.                     break;  // Successfully hit first and last expected value. Test is successful.
  499.                 } else {
  500.                     oddsOfGettingThisFarWithNoMatch = 1.0;  // Reset = odds of finding 2nd match.
  501.                 }
  502.             }
  503.             if (isAUniqueNewNumber) {
  504.                 oddsOfGettingThisFarWithNoMatch = 1.0;  // Restore optimisim, as long as we're seeing new numbers.
  505.                 final var numberOfValuesFound = valuesFound.size();
  506.                 if (numberOfValuesFound >= bound) {
  507.                     fail("Unable to find value(s) " + firstAndLastToFind.toString() + " after " + numberOfTries + " tries."
  508.                             + NL + "  Values found = " + valuesFound.toString()
  509.                             + NL + "  Giving up because the count of unique values found (" + numberOfValuesFound
  510.                             + ") is (at least) equal to the bound value of " + bound + ".");
  511.                 }
  512.             }
  513.         }
  514.  
  515.         // Failure case:   [NEVER happens when "break;" is taken from loop, above.]
  516.         if (!firstAndLastToFind.isEmpty()) {
  517.             fail("Unable to find value(s) " + firstAndLastToFind.toString() + " after " + numberOfTries + " tries."
  518.                     + NL + "  Values found = " + valuesFound.toString()
  519.                     + NL + "  [There's a small chance that this test failure may be in error, as it is testing randomness!]");
  520.         }
  521.     }
  522.  
  523. }
  524.  
  525. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  526. //
  527. // File Name:   tests/ExpectedException.java
  528. //
  529. package p20230510_TestConsoleIO.tests;
  530.  
  531. class ExpectedException extends Error {
  532.     public ExpectedException(final String message) {
  533.         super(message);
  534.     }
  535. }
  536.  
  537. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  538. //
  539. // File Name:   library/RandomSource.java
  540. //
  541. package p20230510_TestConsoleIO.library;
  542.  
  543. import java.util.Random;
  544.  
  545. abstract public class RandomSource {
  546.  
  547.     private RandomSource() {
  548.         throw new IllegalStateException("Not expecting to create instances of this class.");
  549.     }
  550.  
  551.     private static RandomCreatorInterface _creator = () -> {
  552.         return new Random();
  553.     };
  554.  
  555.     public static Random create() {
  556.         return _creator.create();
  557.     }
  558.  
  559.     public static RandomCreatorInterface swapRandomConstructor(final RandomCreatorInterface newRandomCreator) {
  560.         final var oldRandomCreator = _creator;
  561.         _creator = newRandomCreator;
  562.         return oldRandomCreator;
  563.     }
  564.  
  565. }
  566.  
  567. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  568. //
  569. // File Name:   library/RandomCreatorInterface.java
  570. //
  571. package p20230510_TestConsoleIO.library;
  572.  
  573. import java.util.Random;
  574.  
  575. public interface RandomCreatorInterface {
  576.  
  577.     Random create();
  578.  
  579. }
  580.  
  581. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  582. //
  583. // File Name:   library/AbstractMockRandomSwapper.java
  584. //
  585. package p20230510_TestConsoleIO.library;
  586.  
  587. import java.io.Closeable;
  588. import java.io.IOException;
  589. import java.util.Random;
  590.  
  591. import static org.junit.Assert.fail;
  592.  
  593. abstract public class AbstractMockRandomSwapper extends Random implements Closeable {
  594.  
  595.     private final RandomCreatorInterface _oldRandomConstructor;
  596.  
  597.     protected AbstractMockRandomSwapper() {
  598.         _oldRandomConstructor = RandomSource.swapRandomConstructor(() -> {
  599.             return this;
  600.         });
  601.     }
  602.  
  603.     @Override
  604.     public void close() throws IOException {
  605.         final var randomConstructorSwappedOut = RandomSource.swapRandomConstructor(_oldRandomConstructor);
  606.     }
  607.  
  608.     @Override
  609.     final public int nextInt(final int bound) {
  610.         final var returnValue = nextIntImpl(bound);
  611.         if (returnValue < 0 || returnValue >= bound) {
  612.             fail("'Random' value must be in the range of 0 <= value < bound (of " + bound + ")."
  613.                     + "  The value " + returnValue + " is outside this range in class " + this.getClass().getName() + ".");
  614.         }
  615.         return returnValue;
  616.     }
  617.  
  618.     abstract public int nextIntImpl(final int bound);
  619.  
  620. }
  621.  
  622. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  623. //
  624. // File Name:   library/MockRandomSingleValue.java
  625. //
  626. package p20230510_TestConsoleIO.library;
  627.  
  628. public class MockRandomSingleValue extends AbstractMockRandomSwapper {
  629.  
  630.     private int _targetValueToGuess;
  631.  
  632.     public MockRandomSingleValue(final int targetValueToGuess) {
  633.         _targetValueToGuess = targetValueToGuess;
  634.     }
  635.  
  636.     @Override
  637.     public int nextIntImpl(int bound) {
  638.         final var returnValue = _targetValueToGuess - 1;
  639.         _targetValueToGuess = 0;    // invalid value -- for any subsequent calls
  640.         return returnValue;
  641.     }
  642.  
  643. }
  644.  
  645. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  646. //
  647. // File Name:   library/SystemInputOutputTester.java
  648. //
  649. package p20230510_TestConsoleIO.library;
  650.  
  651. import java.io.PrintStream;
  652. import java.util.ArrayList;
  653. import java.util.Iterator;
  654. import java.util.List;
  655.  
  656. public class SystemInputOutputTester {
  657.  
  658.     private final CallbackThrowsException _callback;
  659.     private final List<ExpectedInputOutputStepBase> _expectedInputOutputSteps = new ArrayList<ExpectedInputOutputStepBase>();
  660.     private Iterator<ExpectedInputOutputStepBase> _expectedInputOutputStepIterator = null;
  661.     private final MockInputStream _mockInputStream = new MockInputStream(this);
  662.     private final MockStandardOutputStream _mockStandardOutputStream = new MockStandardOutputStream();
  663.     //protected final MockStandardOutputStream _mockStandardErrorStream = new MockStandardOutputStream(this);
  664.  
  665.     public SystemInputOutputTester(final CallbackThrowsException callback) {
  666.         _callback = callback;
  667.     }
  668.  
  669.     public static SystemInputOutputTester whenCallingThisMethod(final CallbackThrowsException callback) {
  670.         return new SystemInputOutputTester(callback);
  671.     }
  672.  
  673.     public SystemInputOutputTester assertTextOutput(final String expectedOutputMessage) {
  674.         _expectedInputOutputSteps.add(new ExpectedTextOutputStep(_mockStandardOutputStream, expectedOutputMessage));
  675.         return this;
  676.     }
  677.  
  678.     public SystemInputOutputTester provideLineInput(final String lineOfInputText) {
  679.         _expectedInputOutputSteps.add(new ProvideLineInputStep(lineOfInputText));
  680.         return this;
  681.     }
  682.  
  683.     public void assertThatTheMethodReturnsHere() {
  684.         final var oldInput = System.in;
  685.         final var oldOutput = System.out;
  686.         //final var oldError = System.err;
  687.         try {
  688.             System.setIn(_mockInputStream);
  689.             System.setOut(new PrintStream(_mockStandardOutputStream));
  690.             //System.setErr(new PrintStream(_mockStandardErrorStream));
  691.  
  692.             _expectedInputOutputStepIterator = _expectedInputOutputSteps.iterator();
  693.  
  694.             try {
  695.                 _callback.callback();
  696.             } catch (final RuntimeException ex) {
  697.                 throw ex;
  698.             } catch (final Exception ex) {
  699.                 throw new RuntimeException("Nested Exception " + ex.getClass().getName() + ": " + ex.getMessage(), ex);
  700.             }
  701.  
  702.             System.out.flush();
  703.             System.err.flush();
  704.             while (_expectedInputOutputStepIterator.hasNext()) {
  705.                 final var step = _expectedInputOutputStepIterator.next();
  706.                 final var isAfterExit = true;
  707.                 final var didProvideNewUserInput = step.validate(this);
  708.                 if (didProvideNewUserInput) {
  709.                     step.throwException("Program has exited, but the tests say that we should provide further input.");
  710.                 }
  711.             }
  712.  
  713.         } finally {
  714.             System.setIn(oldInput);
  715.             System.setOut(oldOutput);
  716.             //System.setErr(oldError);
  717.         }
  718.     }
  719.  
  720.     public void validateToAndIncludingNextInput() {
  721.         System.out.flush();
  722.         System.err.flush();
  723.         while (_expectedInputOutputStepIterator.hasNext()) {
  724.             final var step = _expectedInputOutputStepIterator.next();
  725.             final var isAfterExit = false;
  726.             final var didProvideNewUserInput = step.validate(this);
  727.             if (didProvideNewUserInput) {
  728.                 break;
  729.             }
  730.         }
  731.     }
  732.  
  733.     protected void provideLineOfInput(final String lineOfInputText) {
  734.         _mockInputStream.provideLineOfInput(lineOfInputText);
  735.     }
  736.  
  737. }
  738.  
  739. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  740. //
  741. // File Name:   library/CallbackThrowsException.java
  742. //
  743. package p20230510_TestConsoleIO.library;
  744.  
  745. public interface CallbackThrowsException {
  746.  
  747.     public void callback() throws Exception;
  748.  
  749. }
  750.  
  751. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  752. //
  753. // File Name:   library/MockStandardOutputStream.java
  754. //
  755. package p20230510_TestConsoleIO.library;
  756.  
  757. import org.jetbrains.annotations.NotNull;
  758.  
  759. import java.io.IOException;
  760. import java.io.OutputStream;
  761.  
  762. import static org.junit.Assert.assertEquals;
  763.  
  764. public class MockStandardOutputStream extends OutputStream {
  765.  
  766.     private final StringBuilder _sequenceOfByteValues = new StringBuilder();
  767.     private final StringBuilder _accumulatedOutput = new StringBuilder();
  768.  
  769.     @Override
  770.     public void write(final int byteValue) throws IOException {
  771.         _sequenceOfByteValues.append((char) byteValue);
  772.     }
  773.  
  774.     public StringBuilder getUpdatedStringBuilder() {
  775.  
  776.         if (_sequenceOfByteValues.length() > 0) {
  777.             final String stringValue = getStringValue();
  778.             _accumulatedOutput.append(stringValue);
  779.         }
  780.  
  781.         return _accumulatedOutput;
  782.     }
  783.  
  784.     @NotNull
  785.     private String getStringValue() {
  786.         final var bytes = new byte[_sequenceOfByteValues.length()];
  787.         final var charArray = _sequenceOfByteValues.toString().toCharArray();
  788.         assertEquals(bytes.length, charArray.length);
  789.         for (int idx = 0; idx < bytes.length; ++idx) {
  790.             bytes[idx] = (byte) charArray[idx];
  791.         }
  792.         _sequenceOfByteValues.setLength(0);
  793.         final var stringValue = new String(bytes);
  794.         return stringValue;
  795.     }
  796.  
  797. }
  798.  
  799. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  800. //
  801. // File Name:   library/MockInputStream.java
  802. //
  803. package p20230510_TestConsoleIO.library;
  804.  
  805. import java.io.IOException;
  806. import java.io.InputStream;
  807.  
  808. public class MockInputStream extends InputStream {
  809.  
  810.     private final SystemInputOutputTester _systemInputOutputTester;
  811.     private final StringBuilder _inputToProvide = new StringBuilder();
  812.  
  813.     public MockInputStream(final SystemInputOutputTester systemInputOutputTester) {
  814.         _systemInputOutputTester = systemInputOutputTester;
  815.     }
  816.  
  817.     @Override
  818.     public int read() throws IOException {
  819.  
  820.         if (_inputToProvide.length() == 0) {
  821.             _systemInputOutputTester.validateToAndIncludingNextInput();
  822.         }
  823.  
  824.         if (_inputToProvide.length() == 0) {
  825.             return -1;  // = End Of File
  826.         } else {
  827.             final var firstCharacter = _inputToProvide.charAt(0);
  828.             _inputToProvide.delete(0, 1);
  829.             return firstCharacter;
  830.         }
  831.     }
  832.  
  833.     @Override
  834.     public int read(final byte byteArray[], final int offset, final int length) throws IOException {
  835.  
  836.         if (_inputToProvide.length() == 0) {
  837.             _systemInputOutputTester.validateToAndIncludingNextInput();
  838.         }
  839.  
  840.         if (_inputToProvide.length() == 0) {
  841.             return -1;  // = End Of File
  842.         } else {
  843.             int charsRead = 0;
  844.             for (; charsRead < length && _inputToProvide.length() > 0; ++charsRead) {
  845.                 final var firstCharacter = _inputToProvide.charAt(0);
  846.                 _inputToProvide.delete(0, 1);
  847.                 byteArray[offset + charsRead] = (byte) firstCharacter;
  848.             }
  849.             return charsRead;
  850.         }
  851.     }
  852.  
  853.     public void provideLineOfInput(final String lineOfInputText) {
  854.         _inputToProvide.append(lineOfInputText);
  855.         _inputToProvide.append('\n');
  856.     }
  857.  
  858. }
  859.  
  860. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  861. //
  862. // File Name:   library/ExpectedInputOutputStepBase.java
  863. //
  864. package p20230510_TestConsoleIO.library;
  865.  
  866. import junit.framework.AssertionFailedError;
  867.  
  868. abstract public class ExpectedInputOutputStepBase {
  869.  
  870.     protected final AssertionFailedError _locationOfAssertionInitialization = new AssertionFailedError(
  871.             "Location of assertion initialization. [Look up in the Test's chained method calls.]");
  872.  
  873.     protected ExpectedInputOutputStepBase() {
  874.     }
  875.  
  876.     public abstract boolean validate(final SystemInputOutputTester systemInputOutputTester);
  877.  
  878.     public void throwException(final String message) {
  879.         final var ex = new AssertionFailedError(message + System.lineSeparator() + "  " + this.toString());
  880.         ex.initCause(_locationOfAssertionInitialization);
  881.         throw ex;
  882.     }
  883.  
  884. }
  885.  
  886. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  887. //
  888. // File Name:   library/ExpectedTextOutputStep.java
  889. //
  890. package p20230510_TestConsoleIO.library;
  891.  
  892. import junit.framework.AssertionFailedError;
  893.  
  894. public class ExpectedTextOutputStep extends ExpectedInputOutputStepBase {
  895.  
  896.     private final MockStandardOutputStream _mockOutputStream;
  897.     private final String _expectedOutputMessage;
  898.  
  899.     public ExpectedTextOutputStep(final MockStandardOutputStream mockOutputStream, final String expectedOutputMessage) {
  900.         _mockOutputStream = mockOutputStream;
  901.         _expectedOutputMessage = expectedOutputMessage;
  902.     }
  903.  
  904.     @Override
  905.     public boolean validate(final SystemInputOutputTester systemInputOutputTester) {
  906.         final var stringBuilder = _mockOutputStream.getUpdatedStringBuilder();
  907.         final var currentStringValue = stringBuilder.toString();
  908.         final var foundAtIndex = currentStringValue.indexOf(_expectedOutputMessage);
  909.         if (foundAtIndex >= 0) {
  910.             // Remove the string value we just found:
  911.             stringBuilder.delete(0, foundAtIndex + _expectedOutputMessage.length());
  912.             // If followed immediately by a new line, remove that too:
  913.             final var newlineCharacterSequence = System.lineSeparator();
  914.             if (stringBuilder.toString().startsWith(newlineCharacterSequence)) {
  915.                 stringBuilder.delete(0, newlineCharacterSequence.length());
  916.             }
  917.             return false;   // Did NOT provide new *input* data.
  918.         } else {
  919.             final var remainingOutputWithNewlinesVisible = currentStringValue
  920.                     .replace("\r\n", "[NL]")
  921.                     .replace("\r", "\\r")
  922.                     .replace("\n", "\\n");
  923.             final var failEx = new AssertionFailedError("Failed to find <" + _expectedOutputMessage + "> in <" + remainingOutputWithNewlinesVisible + ">");
  924.             failEx.initCause(super._locationOfAssertionInitialization);
  925.             throw failEx;
  926.         }
  927.     }
  928. }
  929.  
  930. ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  931. //
  932. // File Name:   library/ProvideLineInputStep.java
  933. //
  934. package p20230510_TestConsoleIO.library;
  935.  
  936. public class ProvideLineInputStep extends ExpectedInputOutputStepBase {
  937.  
  938.     private final String _lineOfInputText;
  939.  
  940.     public ProvideLineInputStep(final String lineOfInputText) {
  941.         _lineOfInputText = lineOfInputText;
  942.     }
  943.  
  944.     @Override
  945.     public boolean validate(final SystemInputOutputTester systemInputOutputTester) {
  946.         systemInputOutputTester.provideLineOfInput(_lineOfInputText);
  947.         return true;   // *DID* provide new *input* data.
  948.     }
  949.  
  950.     @Override
  951.     public String toString() {
  952.         return "ProvideLineInputStep('" + _lineOfInputText + "')";
  953.     }
  954.  
  955. }
  956.  
Add Comment
Please, Sign In to add comment