View Javadoc
1   package org.apache.maven.plugin.surefire.report;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import junit.framework.TestCase;
23  import org.apache.maven.plugin.surefire.booterclient.output.DeserializedStacktraceWriter;
24  import org.apache.maven.surefire.api.report.ReportEntry;
25  import org.apache.maven.surefire.api.report.SimpleReportEntry;
26  import org.apache.maven.surefire.api.report.StackTraceWriter;
27  import org.apache.maven.surefire.shared.utils.xml.Xpp3Dom;
28  import org.apache.maven.surefire.shared.utils.xml.Xpp3DomBuilder;
29  
30  import java.io.ByteArrayOutputStream;
31  import java.io.File;
32  import java.io.FileInputStream;
33  import java.io.IOException;
34  import java.io.InputStreamReader;
35  import java.io.RandomAccessFile;
36  import java.nio.Buffer;
37  import java.nio.ByteBuffer;
38  import java.nio.file.Path;
39  import java.util.Deque;
40  import java.util.HashMap;
41  import java.util.concurrent.ConcurrentHashMap;
42  import java.util.concurrent.atomic.AtomicInteger;
43  
44  import static java.nio.charset.StandardCharsets.UTF_8;
45  import static java.nio.file.Files.readAllLines;
46  import static org.apache.maven.plugin.surefire.report.ReportEntryType.ERROR;
47  import static org.apache.maven.plugin.surefire.report.ReportEntryType.SKIPPED;
48  import static org.apache.maven.plugin.surefire.report.ReportEntryType.SUCCESS;
49  import static org.apache.maven.surefire.api.report.RunMode.NORMAL_RUN;
50  import static org.apache.maven.surefire.api.report.RunMode.RERUN_TEST_AFTER_FAILURE;
51  import static org.apache.maven.surefire.api.util.internal.ObjectUtils.systemProps;
52  import static org.apache.maven.surefire.shared.utils.StringUtils.isEmpty;
53  import static org.assertj.core.api.Assertions.assertThat;
54  import static org.mockito.Mockito.doThrow;
55  import static org.mockito.Mockito.mock;
56  import static org.mockito.Mockito.times;
57  import static org.mockito.Mockito.verify;
58  import static org.mockito.Mockito.when;
59  import static org.powermock.reflect.Whitebox.getInternalState;
60  import static org.powermock.reflect.Whitebox.invokeMethod;
61  import static org.powermock.reflect.Whitebox.setInternalState;
62  import static org.apache.maven.surefire.api.util.internal.StringUtils.NL;
63  
64  /**
65   *
66   */
67  @SuppressWarnings( { "ResultOfMethodCallIgnored", "checkstyle:magicnumber" } )
68  public class StatelessXmlReporterTest
69          extends TestCase
70  {
71      private static final String XSD =
72              "https://maven.apache.org/surefire/maven-surefire-plugin/xsd/surefire-test-report-3.0.xsd";
73      private static final String TEST_ONE = "aTestMethod";
74      private static final String TEST_TWO = "bTestMethod";
75      private static final String TEST_THREE = "cTestMethod";
76      private static final AtomicInteger FOLDER_POSTFIX = new AtomicInteger();
77  
78      private TestSetStats stats;
79      private TestSetStats rerunStats;
80      private File expectedReportFile;
81      private File reportDir;
82  
83      @Override
84      protected void setUp()
85              throws Exception
86      {
87          stats = new TestSetStats( false, true );
88          rerunStats = new TestSetStats( false, true );
89  
90          File basedir = new File( "." );
91          File target = new File( basedir.getCanonicalFile(), "target" );
92          target.mkdir();
93          String reportRelDir = getClass().getSimpleName() + "-" + FOLDER_POSTFIX.incrementAndGet();
94          reportDir = new File( target, reportRelDir );
95          reportDir.mkdir();
96      }
97  
98      @Override
99      protected void tearDown()
100     {
101         if ( expectedReportFile != null )
102         {
103             expectedReportFile.delete();
104         }
105     }
106 
107     public void testFileNameWithoutSuffix()
108     {
109         StatelessXmlReporter reporter =
110                 new StatelessXmlReporter( reportDir, null, false, 0,
111                         new ConcurrentHashMap<String, Deque<WrappedReportEntry>>(), XSD, "3.0",
112                         false, false, false, false );
113         reporter.cleanTestHistoryMap();
114 
115         ReportEntry reportEntry = new SimpleReportEntry( NORMAL_RUN, 0L,
116             getClass().getName(), null, getClass().getName(), null, 12 );
117         WrappedReportEntry testSetReportEntry = new WrappedReportEntry( reportEntry, ReportEntryType.SUCCESS,
118                 12, null, null, systemProps() );
119         stats.testSucceeded( testSetReportEntry );
120         reporter.testSetCompleted( testSetReportEntry, stats );
121 
122         expectedReportFile = new File( reportDir, "TEST-" + getClass().getName() + ".xml" );
123         assertTrue( "Report file (" + expectedReportFile.getAbsolutePath() + ") doesn't exist",
124                 expectedReportFile.exists() );
125     }
126 
127 
128     public void testAllFieldsSerialized()
129             throws IOException
130     {
131         ReportEntry reportEntry = new SimpleReportEntry( NORMAL_RUN, 0L,
132             getClass().getName(), null, TEST_ONE, null, 12 );
133         WrappedReportEntry testSetReportEntry =
134                 new WrappedReportEntry( reportEntry, SUCCESS, 12, null, null, systemProps() );
135         expectedReportFile = new File( reportDir, "TEST-" + getClass().getName() + ".xml" );
136 
137         stats.testSucceeded( testSetReportEntry );
138         StackTraceWriter stackTraceWriter = new DeserializedStacktraceWriter( "A fud msg", "trimmed", "fail at foo" );
139         Utf8RecodingDeferredFileOutputStream stdOut = new Utf8RecodingDeferredFileOutputStream( "fds" );
140         String stdOutPrefix;
141         String stdErrPrefix;
142         if ( defaultCharsetSupportsSpecialChar() )
143         {
144             stdErrPrefix = "std-\u0115rr";
145             stdOutPrefix = "st]]>d-o\u00DCt";
146         }
147         else
148         {
149             stdErrPrefix = "std-err";
150             stdOutPrefix = "st]]>d-out";
151         }
152 
153         stdOut.write( stdOutPrefix + "<null>!\u0020\u0000\u001F", false );
154 
155         Utf8RecodingDeferredFileOutputStream stdErr = new Utf8RecodingDeferredFileOutputStream( "fds" );
156 
157         stdErr.write( stdErrPrefix + "?&-&amp;&#163;\u0020\u0000\u001F", false );
158         WrappedReportEntry t2 = new WrappedReportEntry( new SimpleReportEntry( NORMAL_RUN, 0L,
159             getClass().getName(), null, TEST_TWO, null, stackTraceWriter, 13 ),
160             ReportEntryType.ERROR, 13, stdOut, stdErr );
161 
162         stats.testSucceeded( t2 );
163         StatelessXmlReporter reporter = new StatelessXmlReporter( reportDir, null, false, 0,
164                 new ConcurrentHashMap<String, Deque<WrappedReportEntry>>(), XSD, "3.0", false, false, false, false );
165         reporter.testSetCompleted( testSetReportEntry, stats );
166 
167         FileInputStream fileInputStream = new FileInputStream( expectedReportFile );
168 
169         Xpp3Dom testSuite = Xpp3DomBuilder.build( new InputStreamReader( fileInputStream, UTF_8 ) );
170         assertEquals( "testsuite", testSuite.getName() );
171         Xpp3Dom properties = testSuite.getChild( "properties" );
172         assertEquals( System.getProperties().size(), properties.getChildCount() );
173         Xpp3Dom child = properties.getChild( 1 );
174         assertFalse( isEmpty( child.getAttribute( "value" ) ) );
175         assertFalse( isEmpty( child.getAttribute( "name" ) ) );
176 
177         Xpp3Dom[] testcase = testSuite.getChildren( "testcase" );
178         Xpp3Dom tca = testcase[0];
179         assertEquals( TEST_ONE, tca.getAttribute( "name" ) );
180         assertEquals( "0.012", tca.getAttribute( "time" ) );
181         assertEquals( getClass().getName(), tca.getAttribute( "classname" ) );
182 
183         Xpp3Dom tcb = testcase[1];
184         assertEquals( TEST_TWO, tcb.getAttribute( "name" ) );
185         assertEquals( "0.013", tcb.getAttribute( "time" ) );
186         assertEquals( getClass().getName(), tcb.getAttribute( "classname" ) );
187         Xpp3Dom errorNode = tcb.getChild( "error" );
188         assertNotNull( errorNode );
189         assertEquals( "A fud msg", errorNode.getAttribute( "message" ) );
190         assertEquals( "fail at foo", errorNode.getAttribute( "type" ) );
191         assertEquals( stdOutPrefix + "<null>! &amp#0;&amp#31;", tcb.getChild( "system-out" ).getValue() );
192 
193 
194         assertEquals( stdErrPrefix + "?&-&amp;&#163; &amp#0;&amp#31;", tcb.getChild( "system-err" ).getValue() );
195     }
196 
197     public void testOutputRerunFlakyFailure()
198             throws IOException
199     {
200         WrappedReportEntry testSetReportEntry =
201                 new WrappedReportEntry( new SimpleReportEntry( NORMAL_RUN, 0L,
202                     getClass().getName(), null, TEST_ONE, null, 12 ),
203                         ReportEntryType.SUCCESS, 12, null, null, systemProps() );
204         expectedReportFile = new File( reportDir, "TEST-" + getClass().getName() + ".xml" );
205 
206         stats.testSucceeded( testSetReportEntry );
207         StackTraceWriter stackTraceWriterOne = new DeserializedStacktraceWriter( "A fud msg", "trimmed",
208                 "fail at foo" );
209         StackTraceWriter stackTraceWriterTwo =
210                 new DeserializedStacktraceWriter( "A fud msg two", "trimmed two", "fail at foo two" );
211 
212         String firstRunOut = "first run out";
213         String firstRunErr = "first run err";
214         String secondRunOut = "second run out";
215         String secondRunErr = "second run err";
216 
217         String cls = getClass().getName();
218         WrappedReportEntry testTwoFirstError = new WrappedReportEntry( new SimpleReportEntry( NORMAL_RUN, 0L,
219             cls, null, TEST_TWO, null, stackTraceWriterOne, 5 ),
220             ReportEntryType.ERROR, 5, createStdOutput( firstRunOut ), createStdOutput( firstRunErr ) );
221 
222         WrappedReportEntry testTwoSecondError = new WrappedReportEntry( new SimpleReportEntry(
223             RERUN_TEST_AFTER_FAILURE, 1L, cls, null, TEST_TWO, null, stackTraceWriterTwo, 13 ),
224             ReportEntryType.ERROR, 13, createStdOutput( secondRunOut ), createStdOutput( secondRunErr ) );
225 
226         WrappedReportEntry testThreeFirstRun = new WrappedReportEntry( new SimpleReportEntry( NORMAL_RUN, 2L,
227             cls, null, TEST_THREE, null, stackTraceWriterOne, 13 ),
228             ReportEntryType.FAILURE, 13, createStdOutput( firstRunOut ), createStdOutput( firstRunErr ) );
229 
230         WrappedReportEntry testThreeSecondRun = new WrappedReportEntry( new SimpleReportEntry(
231             RERUN_TEST_AFTER_FAILURE, 3L, cls, null, TEST_THREE, null, stackTraceWriterTwo, 2 ),
232             ReportEntryType.SUCCESS, 2, createStdOutput( secondRunOut ), createStdOutput( secondRunErr ) );
233 
234         stats.testSucceeded( testTwoFirstError );
235         stats.testSucceeded( testThreeFirstRun );
236         rerunStats.testSucceeded( testTwoSecondError );
237         rerunStats.testSucceeded( testThreeSecondRun );
238 
239         StatelessXmlReporter reporter =
240                 new StatelessXmlReporter( reportDir, null, false, 1,
241                         new HashMap<String, Deque<WrappedReportEntry>>(), XSD, "3.0", false, false, false, false );
242 
243         reporter.testSetCompleted( testSetReportEntry, stats );
244         reporter.testSetCompleted( testSetReportEntry, rerunStats );
245 
246         FileInputStream fileInputStream = new FileInputStream( expectedReportFile );
247 
248         Xpp3Dom testSuite = Xpp3DomBuilder.build( new InputStreamReader( fileInputStream, UTF_8 ) );
249         assertEquals( "testsuite", testSuite.getName() );
250         assertEquals( "0.012", testSuite.getAttribute( "time" ) );
251         Xpp3Dom properties = testSuite.getChild( "properties" );
252         assertEquals( System.getProperties().size(), properties.getChildCount() );
253         Xpp3Dom child = properties.getChild( 1 );
254         assertFalse( isEmpty( child.getAttribute( "value" ) ) );
255         assertFalse( isEmpty( child.getAttribute( "name" ) ) );
256 
257         Xpp3Dom[] testcase = testSuite.getChildren( "testcase" );
258         Xpp3Dom testCaseOne = testcase[0];
259         assertEquals( TEST_ONE, testCaseOne.getAttribute( "name" ) );
260         assertEquals( "0.012", testCaseOne.getAttribute( "time" ) );
261         assertEquals( getClass().getName(), testCaseOne.getAttribute( "classname" ) );
262 
263         Xpp3Dom testCaseTwo = testcase[1];
264         assertEquals( TEST_TWO, testCaseTwo.getAttribute( "name" ) );
265         // Run time for a rerun failing test is the run time of the first run
266         assertEquals( "0.005", testCaseTwo.getAttribute( "time" ) );
267         assertEquals( getClass().getName(), testCaseTwo.getAttribute( "classname" ) );
268         Xpp3Dom errorNode = testCaseTwo.getChild( "error" );
269         Xpp3Dom rerunErrorNode = testCaseTwo.getChild( "rerunError" );
270         assertNotNull( errorNode );
271         assertNotNull( rerunErrorNode );
272 
273         assertEquals( "A fud msg", errorNode.getAttribute( "message" ) );
274         assertEquals( "fail at foo", errorNode.getAttribute( "type" ) );
275 
276         // Check rerun error node contains all the information
277         assertEquals( firstRunOut, testCaseTwo.getChild( "system-out" ).getValue() );
278         assertEquals( firstRunErr, testCaseTwo.getChild( "system-err" ).getValue() );
279         assertEquals( secondRunOut, rerunErrorNode.getChild( "system-out" ).getValue() );
280         assertEquals( secondRunErr, rerunErrorNode.getChild( "system-err" ).getValue() );
281         assertEquals( "A fud msg two", rerunErrorNode.getAttribute( "message" ) );
282         assertEquals( "fail at foo two", rerunErrorNode.getAttribute( "type" ) );
283 
284         // Check flaky failure node
285         Xpp3Dom testCaseThree = testcase[2];
286         assertEquals( TEST_THREE, testCaseThree.getAttribute( "name" ) );
287         // Run time for a flaky test is the run time of the first successful run
288         assertEquals( "0.002", testCaseThree.getAttribute( "time" ) );
289         assertEquals( getClass().getName(), testCaseThree.getAttribute( "classname" ) );
290         Xpp3Dom flakyFailureNode = testCaseThree.getChild( "flakyFailure" );
291         assertNotNull( flakyFailureNode );
292         assertEquals( firstRunOut, flakyFailureNode.getChild( "system-out" ).getValue() );
293         assertEquals( firstRunErr, flakyFailureNode.getChild( "system-err" ).getValue() );
294         // system-out and system-err should not be present for flaky failures
295         assertNull( testCaseThree.getChild( "system-out" ) );
296         assertNull( testCaseThree.getChild( "system-err" ) );
297     }
298 
299     public void testOutputRerunFlakyAssumption()
300         throws IOException
301     {
302         expectedReportFile = new File( reportDir, "TEST-" + getClass().getName() + ".xml" );
303 
304         StackTraceWriter stackTraceWriterOne = new DeserializedStacktraceWriter( "A fud msg", "trimmed",
305             "fail at foo" );
306 
307         StackTraceWriter stackTraceWriterTwo =
308             new DeserializedStacktraceWriter( "A fud msg two", "trimmed two", "fail at foo two" );
309 
310         String firstRunOut = "first run out";
311         String firstRunErr = "first run err";
312         String secondRunOut = "second run out";
313         String secondRunErr = "second run err";
314 
315         WrappedReportEntry testTwoFirstError =
316             new WrappedReportEntry( new SimpleReportEntry( NORMAL_RUN, 1L, getClass().getName(), null, TEST_TWO, null,
317                 stackTraceWriterOne, 5 ), ERROR, 5, createStdOutput( firstRunOut ),
318                 createStdOutput( firstRunErr ) );
319 
320         stats.testSucceeded( testTwoFirstError );
321 
322         WrappedReportEntry testTwoSecondError =
323             new WrappedReportEntry( new SimpleReportEntry( RERUN_TEST_AFTER_FAILURE, 1L, getClass().getName(), null,
324                 TEST_TWO, null,
325                 stackTraceWriterTwo, 13 ), SKIPPED, 13, createStdOutput( secondRunOut ),
326                 createStdOutput( secondRunErr ) );
327 
328         rerunStats.testSucceeded( testTwoSecondError );
329 
330         StatelessXmlReporter reporter =
331             new StatelessXmlReporter( reportDir, null, false, 1,
332                 new HashMap<>(), XSD, "3.0", false, false, false, false );
333 
334         WrappedReportEntry testSetReportEntry =
335             new WrappedReportEntry( new SimpleReportEntry( RERUN_TEST_AFTER_FAILURE, 1L, getClass().getName(), null,
336                 null, null,
337                 stackTraceWriterOne, 5 ), ERROR, 20, createStdOutput( firstRunOut ),
338                 createStdOutput( firstRunErr ) );
339 
340         reporter.testSetCompleted( testSetReportEntry, stats );
341         reporter.testSetCompleted( testSetReportEntry, rerunStats );
342 
343         FileInputStream fileInputStream = new FileInputStream( expectedReportFile );
344 
345         Xpp3Dom testSuite = Xpp3DomBuilder.build( new InputStreamReader( fileInputStream, UTF_8 ) );
346         assertEquals( "testsuite", testSuite.getName() );
347         assertEquals( "0.02", testSuite.getAttribute( "time" ) );
348 
349         Xpp3Dom[] testcase = testSuite.getChildren( "testcase" );
350         assertEquals( 1, testcase.length );
351         Xpp3Dom testCaseOne = testcase[0];
352         assertEquals( getClass().getName(), testCaseOne.getAttribute( "classname" ) );
353         assertEquals( TEST_TWO, testCaseOne.getAttribute( "name" ) );
354         assertEquals( "0.005", testCaseOne.getAttribute( "time" ) );
355 
356         Xpp3Dom[] testCaseElements = testCaseOne.getChildren();
357         assertEquals( 3, testCaseElements.length );
358         assertEquals( "error", testCaseElements[0].getName() );
359         assertEquals( "system-out", testCaseElements[1].getName() );
360         assertEquals( "system-err", testCaseElements[2].getName() );
361         long linesWithComments = readAllLines( expectedReportFile.toPath(), UTF_8 )
362             .stream()
363             .filter( line -> line.contains( "<!-- a skipped test execution in re-run phase -->" ) )
364             .count();
365         assertEquals( 1, linesWithComments );
366     }
367 
368     public void testNoWritesOnDeferredFile() throws Exception
369     {
370         Utf8RecodingDeferredFileOutputStream out = new Utf8RecodingDeferredFileOutputStream( "test" );
371         out.free();
372         out.write( "a", false );
373         assertThat( (boolean) getInternalState( out, "isDirty" ) )
374             .isFalse();
375     }
376 
377     public void testLengthOnDeferredFile() throws Exception
378     {
379         Utf8RecodingDeferredFileOutputStream out = new Utf8RecodingDeferredFileOutputStream( "test" );
380 
381         assertThat( out.getByteCount() ).isZero();
382 
383         File f = File.createTempFile( "test", "tmp" );
384         RandomAccessFile storage = new RandomAccessFile( f, "rw" );
385         setInternalState( out, "storage", storage );
386         setInternalState( out, "file", f.toPath() );
387         storage.writeByte( 0 );
388         storage.getFD().sync();
389         assertThat( out.getByteCount() ).isEqualTo( 1 );
390 
391         storage.close();
392         assertThat( f.delete() ).isTrue();
393         assertThat( out.getByteCount() ).isZero();
394         out.free();
395     }
396 
397     @SuppressWarnings( "checkstyle:magicnumber" )
398     public void testWritesOnDeferredFile() throws Exception
399     {
400         Utf8RecodingDeferredFileOutputStream out = new Utf8RecodingDeferredFileOutputStream( "test" );
401         for ( int i = 0; i < 33_000; i++ )
402         {
403             out.write( "A", false );
404             out.write( "B", true );
405         }
406         out.write( null, false );
407         out.write( null, true );
408 
409         assertThat( out.getByteCount() )
410             .isEqualTo( 33_000 * ( 1 + 1 + NL.length() ) + 4 + 4 + NL.length() );
411 
412         StringBuilder expectedContent = new StringBuilder( 150_000 );
413         for ( int i = 0; i < 33_000; i++ )
414         {
415             expectedContent.append( 'A' ).append( 'B' ).append( NL );
416         }
417         expectedContent.append( "null" ).append( "null" ).append( NL );
418         ByteArrayOutputStream read = new ByteArrayOutputStream( 150_000 );
419         out.writeTo( read );
420         assertThat( read.toString() )
421             .isEqualTo( expectedContent.toString() );
422 
423         out.free();
424     }
425 
426     public void testFreeOnDeferredFile() throws Exception
427     {
428         Utf8RecodingDeferredFileOutputStream out = new Utf8RecodingDeferredFileOutputStream( "test" );
429         setInternalState( out, "cache", ByteBuffer.allocate( 0 ) );
430         Path path = mock( Path.class );
431         File file = mock( File.class );
432         when( path.toFile() ).thenReturn( file );
433         setInternalState( out, "file", path );
434         RandomAccessFile storage = mock( RandomAccessFile.class );
435         doThrow( IOException.class ).when( storage ).close();
436         setInternalState( out, "storage", storage );
437         out.free();
438         assertThat( (boolean) getInternalState( out, "closed" ) ).isTrue();
439         verify( file, times( 1 ) ).deleteOnExit();
440     }
441 
442     public void testCacheOnDeferredFile() throws Exception
443     {
444         Utf8RecodingDeferredFileOutputStream out = new Utf8RecodingDeferredFileOutputStream( "test" );
445         byte[] b1 = invokeMethod( out, "getLargeCache", 1 );
446         byte[] b2 = invokeMethod( out, "getLargeCache", 1 );
447         assertThat( b1 ).isSameAs( b2 );
448         assertThat( b1 ).hasSize( 1 );
449 
450         byte[] b3 = invokeMethod( out, "getLargeCache", 2 );
451         assertThat( b3 ).isNotSameAs( b1 );
452         assertThat( b3 ).hasSize( 2 );
453 
454         byte[] b4 = invokeMethod( out, "getLargeCache", 1 );
455         assertThat( b4 ).isSameAs( b3 );
456         assertThat( b3 ).hasSize( 2 );
457     }
458 
459     public void testSyncOnDeferredFile() throws Exception
460     {
461         Utf8RecodingDeferredFileOutputStream out = new Utf8RecodingDeferredFileOutputStream( "test" );
462         Buffer cache = ByteBuffer.wrap( new byte[] {1, 2, 3} );
463         cache.position( 3 );
464         setInternalState( out, "cache", cache );
465         assertThat( (boolean) getInternalState( out, "isDirty" ) ).isFalse();
466         setInternalState( out, "isDirty", true );
467         File file = new File( reportDir, "test" );
468         setInternalState( out, "file", file.toPath() );
469         RandomAccessFile storage = new RandomAccessFile( file, "rw" );
470         setInternalState( out, "storage", storage );
471         invokeMethod( out, "sync" );
472         assertThat( (boolean) getInternalState( out, "isDirty" ) ).isFalse();
473         storage.seek( 0L );
474         assertThat( storage.read() ).isEqualTo( 1 );
475         assertThat( storage.read() ).isEqualTo( 2 );
476         assertThat( storage.read() ).isEqualTo( 3 );
477         assertThat( storage.read() ).isEqualTo( -1 );
478         assertThat( storage.length() ).isEqualTo( 3L );
479         assertThat( cache.position() ).isEqualTo( 0 );
480         assertThat( cache.limit() ).isEqualTo( 3 );
481         storage.seek( 3L );
482         invokeMethod( out, "sync" );
483         assertThat( (boolean) getInternalState( out, "isDirty" ) ).isFalse();
484         assertThat( storage.length() ).isEqualTo( 3L );
485         assertThat( out.getByteCount() ).isEqualTo( 3L );
486         assertThat( (boolean) getInternalState( out, "closed" ) ).isFalse();
487         out.free();
488         assertThat( (boolean) getInternalState( out, "closed" ) ).isTrue();
489         //todo assertThat( file ).doesNotExist();
490         out.free();
491         assertThat( (boolean) getInternalState( out, "closed" ) ).isTrue();
492     }
493 
494     private boolean defaultCharsetSupportsSpecialChar()
495     {
496         // some charsets are not able to deal with \u0115 on both ways of the conversion
497         return "\u0115\u00DC".equals( new String( "\u0115\u00DC".getBytes() ) );
498     }
499 
500     private Utf8RecodingDeferredFileOutputStream createStdOutput( String content )
501             throws IOException
502     {
503         Utf8RecodingDeferredFileOutputStream stdOut = new Utf8RecodingDeferredFileOutputStream( "fds2" );
504         stdOut.write( content, false );
505         return stdOut;
506     }
507 }