View Javadoc
1   package org.djutils.decoderdumper;
2   
3   import static org.junit.Assert.assertEquals;
4   import static org.junit.Assert.assertTrue;
5   import static org.junit.Assert.fail;
6   
7   import java.io.BufferedOutputStream;
8   import java.io.ByteArrayOutputStream;
9   import java.io.IOException;
10  import java.io.InputStream;
11  import java.io.OutputStream;
12  import java.io.PrintStream;
13  
14  import org.djutils.logger.CategoryLogger;
15  import org.junit.Test;
16  
17  /**
18   * Tests for the decoder/dumper package.
19   * <p>
20   * Copyright (c) 2013-2019 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
21   * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
22   * <p>
23   * @version $Revision$, $LastChangedDate$, by $Author$, initial version Jan 3, 2019 <br>
24   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
25   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
26   * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
27   */
28  public class DecoderDumperTests
29  {
30  
31      /**
32       * Test the Hex decoder and dumper classes.
33       * @throws InterruptedException if that happens; this test has failed.
34       * @throws IOException if that happens; this test has failed.
35       */
36      @Test
37      public final void testHexDumper() throws InterruptedException, IOException
38      {
39          assertEquals("Empty input yields empty output", "", HexDumper.hexDumper(new byte[] {}));
40          byte[] input = new byte[] { 1, 2 };
41          String output = HexDumper.hexDumper(input);
42          assertTrue("Output starts with address \"00000000: \"", output.startsWith("00000000: "));
43          for (int length = 1; length < 100; length++)
44          {
45              input = new byte[length];
46              assertTrue("Output ends on newline", HexDumper.hexDumper(input).endsWith("\n"));
47          }
48          input = new byte[1];
49          for (int value = 0; value < 256; value++)
50          {
51              input[0] = (byte) value;
52              output = HexDumper.hexDumper(input);
53              // System.out.print(String.format("%3d -> %s", value, output));
54              assertTrue("Output contains hex value of the only input byte embedded between spaces",
55                      output.contains(String.format(" %02x ", value)));
56          }
57          assertEquals("output of 16 byte input fills one lines", 1,
58                  HexDumper.hexDumper(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }).split("\n").length);
59          assertEquals(
60                  "output of 17 byte input fills two lines",
61                  2,
62                  HexDumper.hexDumper(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }).split("\n").length);
63          assertTrue("address offset is printed at start of output", HexDumper.hexDumper(0x12345, new byte[] { 0, 1 })
64                  .startsWith("00012340"));
65          Dumper<HexDumper> hd = new HexDumper(0x12345);
66          assertTrue("toString makes some sense", hd.toString().startsWith("HexDumper"));
67  
68          ByteArrayOutputStream baos = new ByteArrayOutputStream();
69          hd = new HexDumper().setOutputStream(baos);
70          for (int i = 0; i < 100; i++)
71          {
72              hd.append((byte) i);
73              // System.out.println("i=" + i + ", hd=" + hd + " baos=" + baos);
74              assertEquals("Number of lines check", Math.max(1, (i + 1) / 16), baos.toString().split("\n").length);
75          }
76          // System.out.println(hd.getDump());
77          for (int i = 33; i < 127; i++)
78          {
79              String dump = HexDumper.hexDumper(new byte[] { (byte) i });
80              String letter = "" + (char) i;
81              String trimmed = dump.trim();
82              String lastLetter = trimmed.substring(trimmed.length() - 1);
83              // System.out.print("i=" + i + " letter=" + letter + ", output is: " + dump);
84              assertEquals("Output ends with the provided printable character", letter, lastLetter);
85          }
86          baos.reset();
87          hd = new HexDumper().addDecoder(0, new TimeStamper()).setOutputStream(baos);
88          long startTimeStamp = System.currentTimeMillis();
89          hd.append((byte) 10);
90          long endTimeStamp = System.currentTimeMillis();
91          Thread.sleep(100);
92          hd.append((byte) 20);
93          hd.flush();
94          String result = baos.toString();
95          int spacePosition = result.indexOf(" ");
96          long recorded = Long.parseLong(result.substring(0, spacePosition).replace(".", "").replace(",", ""));
97          assertTrue("Time stamp should be within interval", startTimeStamp <= recorded);
98          assertTrue("Time stamp should be within interval", endTimeStamp >= recorded);
99          hd = new HexDumper().setOutputStream(new OutputStream()
100         {
101 
102             @Override
103             public void write(final int b) throws IOException
104             {
105                 throw new IOException("testing exception handling");
106             }
107         });
108         try
109         {
110             hd.append(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 });
111             fail("Writing sufficient number of bytes to output that throws an exception should have thrown an exception");
112         }
113         catch (Exception exception)
114         {
115             // Ignore expected exception
116         }
117         baos.reset();
118         hd = new HexDumper().setOutputStream(baos);
119         hd.append(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 });
120         // By now there should be something in the ByteArrayOutputStream
121         assertTrue("ByteArrayOutputStream contains start of hex dump", baos.toString().startsWith("00000000: 00 "));
122         baos.reset();
123         PrintStream oldErrOutput = System.err;
124         PrintStream ps = new PrintStream(new BufferedOutputStream(baos));
125         System.setErr(ps);
126         // Redirect the output to a CategoryLogger
127         hd = new HexDumper().setOutputStream(new OutputStream()
128         {
129             /** The string builder. */
130             private StringBuilder sb = new StringBuilder();
131 
132             @Override
133             public void write(final int b) throws IOException
134             {
135                 if ('\n' == b)
136                 {
137                     CategoryLogger.always().error(this.sb.toString());
138                     this.sb.setLength(0);
139                 }
140                 else
141                 {
142                     this.sb.append((char) b);
143                 }
144             }
145         });
146         for (int value = 0; value < 256; value++)
147         {
148             input[0] = (byte) value;
149             hd.append(input);
150         }
151         Thread.sleep(200);
152         ps.close();
153         System.setErr(oldErrOutput);
154         result = baos.toString();
155         assertEquals("Result should be 16 lines", 16, result.split("\n").length);
156         // System.out.print("baos contains:\n" + result);
157         baos.reset();
158         hd = new HexDumper().setOutputStream(baos).append(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }, 4, 2);
159         hd.flush();
160         result = baos.toString();
161         assertTrue("start and length parameter select the correct bytes", result.startsWith("00000000: 05 06    "));
162         baos.reset();
163         hd = new HexDumper().setOutputStream(baos);
164         hd.append(new InputStream()
165         {
166             private int callCount = 0;
167 
168             @Override
169             public int read() throws IOException
170             {
171                 if (this.callCount < 10)
172                 {
173                     return this.callCount++;
174                 }
175                 return -1;
176             }
177         });
178         hd.flush();
179         result = baos.toString();
180         // System.out.println(result);
181         assertTrue("Ten bytes should have been accumulated",
182                 result.startsWith("00000000: 00 01 02 03 04 05 06 07  08 09    "));
183         baos.reset();
184         hd = new HexDumper().setSuppressMultipleIdenticalLines(true).setOutputStream(baos);
185         for (int line = 0; line < 20; line++)
186         {
187             hd.append(new byte[] { 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57 });
188         }
189         hd.flush();
190         // System.out.println(baos);
191         assertEquals("Suppression reduced the output to three lines", 3, baos.toString().split("\n").length);
192         // System.out.println(baos);
193         baos.reset();
194         hd = new HexDumper().setSuppressMultipleIdenticalLines(true).setOutputStream(baos);
195         for (int line = 0; line < 20; line++)
196         {
197             hd.append(new byte[] { 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57 });
198         }
199         hd.append(new byte[] { 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56 });
200         hd.flush();
201         assertEquals("Suppression reduced the output to four lines", 4, baos.toString().split("\n").length);
202         // System.out.println(baos);
203         baos.reset();
204         hd = new HexDumper().setSuppressMultipleIdenticalLines(true).setOutputStream(baos);
205         for (int line = 0; line < 20; line++)
206         {
207             hd.append(new byte[] { 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57 });
208         }
209         hd.append(new byte[] { 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 99 });
210         hd.flush();
211         assertEquals("Suppression reduced the output to four lines", 4, baos.toString().split("\n").length);
212         // System.out.println(baos);
213         // FIXME: not exhaustively testing switching compression on and off between append calls.
214         assertTrue("TimeStamper had decent toString method", new TimeStamper().toString().startsWith("TimeStamper ["));
215     }
216 
217     /**
218      * Test the Base64 decoder and dumper classes.
219      */
220     @Test
221     public void testBase64Dumper()
222     {
223         assertEquals("Empty input yields empty output", "", Base64Dumper.base64Dumper(new byte[] {}));
224         byte[] input = new byte[] { 1, 2 };
225         String output = HexDumper.hexDumper(input);
226         assertTrue("Output starts with address \"00000000: \"", output.startsWith("00000000: "));
227         for (int length = 1; length < 100; length++)
228         {
229             input = new byte[length];
230             assertTrue("Output ends on newline (even though the input is invalid)", HexDumper.hexDumper(input)
231                     .endsWith("\n"));
232         }
233         // Generate many possible 24-bit values; then construct the base64 string that would generate the 3 bytes
234         for (int pattern = 0; pattern < 256 * 256 * 256; pattern += 259)
235         {
236             input = new byte[4];
237             for (int index = 0; index < 4; index++)
238             {
239                 input[index] = encode((pattern >> (18 - 6 * index)) & 0x3f);
240             }
241             output = Base64Dumper.base64Dumper(input).substring(30);
242             // System.out.println("input \"" + pattern +"\", output \"" + output + "\"");
243             for (int index = 0; index < 3; index++)
244             {
245                 int theByte = Integer.parseInt(output.substring(index * 3, index * 3 + 2), 16);
246                 int expectedByte = (pattern >> (16 - 8 * index)) & 0xff;
247                 assertEquals("Reconstructed byte matches corresponding byte in pattern", expectedByte, theByte);
248             }
249         }
250         // Generate all possible 8-bit values and pad with two = signs
251         for (int pattern = 0; pattern < 255; pattern++)
252         {
253             input = new byte[4];
254             for (int index = 0; index < 2; index++)
255             {
256                 input[index] = encode(((pattern << 16) >> (18 - 6 * index)) & 0x3f);
257             }
258             input[2] = (byte) 61;
259             input[3] = (byte) 61;
260             output = Base64Dumper.base64Dumper(input).substring(30);
261             // System.out.println("input " + pattern + ", base64=" + Arrays.toString(input) + ", output \"" + output + "\"");
262             int theByte = Integer.parseInt(output.substring(0, 2), 16);
263             assertEquals("Reconstructed byte matches corresponding byte in patten", pattern, theByte);
264             assertTrue("Rest of result starts with at least 10 spaces", output.substring(2).startsWith("          "));
265         }
266         // Generate all possible 16-bit values and pad with one = sign
267         for (int pattern = 0; pattern < 255 * 255; pattern++)
268         {
269             input = new byte[4];
270             for (int index = 0; index < 3; index++)
271             {
272                 input[index] = encode(((pattern << 8) >> (18 - 6 * index)) & 0x3f);
273             }
274             input[3] = (byte) 61;
275             output = Base64Dumper.base64Dumper(input).substring(30);
276             // System.out.println("input " + pattern + ", base64=" + Arrays.toString(input) + ", output \"" + output + "\"");
277             for (int index = 0; index < 2; index++)
278             {
279                 int theByte = Integer.parseInt(output.substring(index * 3, index * 3 + 2), 16);
280                 int expectedByte = (pattern >> (8 - 8 * index)) & 0xff;
281                 assertEquals("Reconstructed byte matches corresponding byte in pattern", expectedByte, theByte);
282             }
283             assertTrue("Rest of result starts with at least 10 spaces", output.substring(5).startsWith("          "));
284         }
285         assertTrue("toString makes some sense", new Base64Dumper().toString().startsWith("Base64Dumper"));
286         // White space in base64 encoded data should be ignored
287         String base64 = "c3VyZS4=";
288         String expectedResult = Base64Dumper.base64Dumper(base64.getBytes()).substring(30);
289         // System.out.print("reference: " + expectedResult);
290         for (int pos = 0; pos <= base64.length(); pos++)
291         {
292             for (String insert : new String[] { " ", "\t", "\n", "\n\t" })
293             {
294                 String modified = base64.substring(0, pos) + insert + base64.substring(pos);
295                 String result = Base64Dumper.base64Dumper(modified.getBytes()).substring(30);
296                 // System.out.print("result:    " + result);
297                 assertEquals("Extra space in input does not change output", expectedResult, result);
298             }
299         }
300         // Error character in input is (currently) silently ignored
301         String result = Base64Dumper.base64Dumper(("!" + base64).getBytes()).substring(30);
302         // System.out.print("result:    " + result);
303         assertEquals("bad char in input is (currently) ignored", expectedResult, result);
304     }
305 
306     /**
307      * Base64 encode one 6-bit value
308      * @param value int; value in the range 0..63
309      * @return byte; the encoded value
310      */
311     private byte encode(final int value)
312     {
313         if (value < 0 || value > 63)
314         {
315             throw new Error("Bad input: " + value);
316         }
317         if (value < 26)
318         {
319             return (byte) (value + 65);
320         }
321         else if (value < 52)
322         {
323             return (byte) (value - 26 + 97);
324         }
325         else if (value < 62)
326         {
327             return (byte) (value - 52 + 48);
328         }
329         else if (value == 62)
330         {
331             return (byte) 43;
332         }
333         else if (value == 63)
334         {
335             return (byte) 47;
336         }
337         throw new Error("Bad input: " + value);
338     }
339 
340 }