1 package org.broadinstitute.hellbender.testutils;
2 
3 import htsjdk.samtools.util.Log;
4 import org.apache.logging.log4j.LogManager;
5 import org.apache.logging.log4j.Logger;
6 import org.broadinstitute.hellbender.Main;
7 import org.broadinstitute.hellbender.engine.GATKPath;
8 import org.broadinstitute.hellbender.engine.spark.SparkContextFactory;
9 import org.broadinstitute.hellbender.exceptions.UserException;
10 import org.broadinstitute.hellbender.utils.LoggingUtils;
11 import org.broadinstitute.hellbender.utils.gcs.BucketUtils;
12 import org.broadinstitute.hellbender.utils.io.IOUtils;
13 import org.broadinstitute.hellbender.utils.runtime.ProcessController;
14 import org.broadinstitute.hellbender.utils.runtime.ProcessOutput;
15 import org.broadinstitute.hellbender.utils.runtime.ProcessSettings;
16 import org.testng.Assert;
17 import org.testng.Reporter;
18 import org.testng.annotations.BeforeSuite;
19 
20 import java.io.ByteArrayOutputStream;
21 import java.io.File;
22 import java.io.IOException;
23 import java.io.PrintStream;
24 import java.nio.file.FileSystem;
25 import java.nio.file.Files;
26 import java.nio.file.Path;
27 import java.nio.file.Paths;
28 import java.util.*;
29 import java.util.function.BiConsumer;
30 import java.util.function.Consumer;
31 
32 
33 /**
34  * This is the base test class for all of our test cases.  All test cases should extend from this
35  * class; it sets up the logger, and resolves the location of directories that we rely on.
36  */
37 public abstract class BaseTest {
38 
39     static {
40         // set properties for the local Spark runner
41         System.setProperty("dataflow.spark.test.reuseSparkContext", "true");
SparkContextFactory.enableTestSparkContext()42         SparkContextFactory.enableTestSparkContext();
43         System.setProperty("picard.useLegacyParser", "false");
44     }
45 
46     /**
47      * run an external process and assert that it finishes with exit code 0
48      * @param processController a ProcessController to use
49      * @param command command to run, the 0th element must be the executable name
50      */
runProcess(final ProcessController processController, final String[] command)51     public static void runProcess(final ProcessController processController, final String[] command) {
52         runProcess(processController, command, "Process exited with non-zero value. Command: "+ Arrays.toString(command) + "\n");
53     }
54 
55     /**
56      * run an external process and assert that it finishes with exit code 0
57      * @param processController a ProcessController to use
58      * @param command command to run, the 0th element must be the executable name
59      * @param message error message to display on failure
60      */
runProcess(final ProcessController processController, final String[] command, final String message)61     public static void runProcess(final ProcessController processController, final String[] command, final String message) {
62         runProcess(processController, command, null, message);
63     }
64 
65     /**
66      * run an external process and assert that it finishes with exit code 0
67      * @param processController a ProcessController to use
68      * @param command command to run, the 0th element must be the executable name
69      * @param environment what to use as the process environment variables
70      * @param message error message to display on failure
71      */
runProcess(final ProcessController processController, final String[] command, final Map<String, String> environment, final String message)72     public static void runProcess(final ProcessController processController, final String[] command, final Map<String, String> environment, final String message) {
73         final ProcessSettings prs = new ProcessSettings(command);
74         prs.getStderrSettings().printStandard(true);
75         prs.getStdoutSettings().printStandard(true);
76         prs.setEnvironment(environment);
77         final ProcessOutput output = processController.exec(prs);
78         Assert.assertEquals(output.getExitValue(), 0, message);
79     }
80 
81     /**
82      * Spawn a new jvm with the same classpath as this one and run a gatk CommandLineProgram
83      * This is useful for running tests that require changing static state that is not allowed to change during
84      * a tool run but which needs to be changed to test some condition.
85      *
86      * @param toolName CommandLineProgram to run
87      * @param arguments arguments to provide to the tool
88      */
runToolInNewJVM(String toolName, ArgumentsBuilder arguments)89     public static void runToolInNewJVM(String toolName, ArgumentsBuilder arguments){
90         final String javaHome = System.getProperty("java.home");
91         final String javaBin = javaHome + File.separator + "bin" + File.separator + "java";
92         final String classpath = System.getProperty("java.class.path");;
93         final List<String> baseCommand = new ArrayList<>(Arrays.asList(
94                 javaBin,
95                 "-cp", classpath,
96                 Main.class.getName(),
97                 toolName));
98         baseCommand.addAll(arguments.getArgsList());
99 
100         runProcess(ProcessController.getThreadLocal(), baseCommand.toArray(new String[0]));
101     }
102 
103     @BeforeSuite
setTestVerbosity()104     public void setTestVerbosity(){
105         LoggingUtils.setLoggingLevel(Log.LogLevel.WARNING);
106     }
107 
108     public static final Logger logger = LogManager.getLogger("org.broadinstitute.gatk");
109 
110     /**
111      * name of the google cloud project that stores the data and will run the code
112      * @return HELLBENDER_TEST_PROJECT env. var if defined, throws otherwise.
113      */
getGCPTestProject()114     public static String getGCPTestProject() {
115         return getNonNullEnvironmentVariable("HELLBENDER_TEST_PROJECT");
116     }
117 
118     /**
119      * A writable GCS path where java files can be cached and temporary test files can be written,
120      * of the form gs://bucket/, or gs://bucket/path/.
121      * @return HELLBENDER_TEST_STAGING env. var if defined, throws otherwise.
122      */
getGCPTestStaging()123     public static String getGCPTestStaging() {
124         return getNonNullEnvironmentVariable("HELLBENDER_TEST_STAGING");
125     }
126 
127     /**
128      *  A GCS path where the test inputs are stored.
129      *
130      *  The value of HELLBENDER_TEST_INPUTS should end in a "/" (for example, "gs://hellbender/test/resources/")
131      *
132      *  @return HELLBENDER_TEST_INPUTS env. var if defined, throws otherwise.
133      */
getGCPTestInputPath()134     public static String getGCPTestInputPath() {
135         return getNonNullEnvironmentVariable("HELLBENDER_TEST_INPUTS");
136     }
137 
138     /**
139      *  A path where the test inputs for the Funcotator LargeDataValidationTest are stored.
140      *
141      *  The value of FUNCOTATOR_LARGE_TEST_INPUTS should end in a "/" (for example, "gs://hellbender/funcotator/test/resources/")
142      *
143      *  @return FUNCOTATOR_LARGE_TEST_INPUTS env. var if defined, throws otherwise.
144      */
getFuncotatorLargeDataValidationTestInputPath()145     public static String getFuncotatorLargeDataValidationTestInputPath() {
146         return getNonNullEnvironmentVariable("FUNCOTATOR_LARGE_TEST_INPUTS");
147     }
148 
149     /**
150      * A local path where the service account credentials are stored
151      * @return GOOGLE_APPLICATION_CREDENTIALS env. var if defined, throws otherwise.
152      */
getGoogleServiceAccountKeyPath()153     public static String getGoogleServiceAccountKeyPath() {
154       return getNonNullEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS");
155     }
156 
getNonNullEnvironmentVariable(String envVarName)157     protected static String getNonNullEnvironmentVariable(String envVarName) {
158         String value = System.getenv(envVarName);
159         if (null == value) {
160             throw new UserException("For this test, please define environment variable \""+envVarName+"\"");
161         }
162         return value;
163     }
164 
165     /**
166      * Returns the location of the resource directory for the command line program.
167      */
getToolTestDataDir()168     public String getToolTestDataDir(){
169         return "src/test/resources/" +this.getClass().getPackage().getName().replace(".","/") +"/" + getTestedClassName() + "/";
170     }
171 
172     /**
173      * Returns the name of the class being tested.
174      * The default implementation takes the simple name of the test class and removes the trailing "Test".
175      * Override if needed.
176      */
getTestedClassName()177     public String getTestedClassName(){
178         if (getClass().getSimpleName().contains("IntegrationTest"))
179             return getClass().getSimpleName().replaceAll("IntegrationTest$", "");
180         else if (getClass().getSimpleName().contains("UnitTest"))
181             return getClass().getSimpleName().replaceAll("UnitTest$", "");
182         else
183             return getClass().getSimpleName().replaceAll("Test$", "");
184     }
185 
186     /**
187      * @param fileName the name of a file
188      * @return a File resolved using getToolTestDataDir as the parent and fileName
189      */
getTestFile(String fileName)190     public File getTestFile(String fileName) {
191         return new File(getToolTestDataDir(), fileName);
192     }
193 
194     /**
195      * @param fileName the name of a file
196      * @return a File resolved using getToolTestDataDir as the parent and fileName
197      */
getTestFileGATKPath(String fileName)198     public GATKPath getTestFileGATKPath(String fileName) {
199         return new GATKPath(String.format("%s/%s", getToolTestDataDir(), fileName));
200     }
201 
202     /**
203      * Simple generic utility class to creating TestNG data providers:
204      *
205      * 1: inherit this class, as in
206      *
207      *      private class SummarizeDifferenceTest extends TestDataProvider {
208      *         public SummarizeDifferenceTest() {
209      *           super(SummarizeDifferenceTest.class);
210      *         }
211      *         ...
212      *      }
213      *
214      * Provide a reference to your class to the TestDataProvider constructor.
215      *
216      * 2: Create instances of your subclass.  Return from it the call to getTests, providing
217      * the class type of your test
218      *
219      * <code>
220      * {@literal @}DataProvider(name = "summaries")
221      * public Object[][] createSummaries() {
222      *   new SummarizeDifferenceTest().addDiff("A", "A").addSummary("A:2");
223      *   new SummarizeDifferenceTest().addDiff("A", "B").addSummary("A:1", "B:1");
224      *   return SummarizeDifferenceTest.getTests(SummarizeDifferenceTest.class);
225      * }
226      * </code>
227      *
228      * This class magically tracks created objects of this
229      */
230     public static class TestDataProvider {
231         private static final Map<Class<?>, List<Object>> tests = new LinkedHashMap<>();
232         protected String name;
233 
234         /**
235          * Create a new TestDataProvider instance bound to the class variable C
236          */
TestDataProvider(Class<?> c, String name)237         public TestDataProvider(Class<?> c, String name) {
238             if ( ! tests.containsKey(c) )
239                 tests.put(c, new ArrayList<>());
240             tests.get(c).add(this);
241             this.name = name;
242         }
243 
TestDataProvider(Class<?> c)244         public TestDataProvider(Class<?> c) {
245             this(c, "");
246         }
247 
setName(final String name)248         public void setName(final String name) {
249             this.name = name;
250         }
251 
252         /**
253          * Return all of the data providers in the form expected by TestNG of type class C
254          */
getTests(Class<?> c)255         public static Object[][] getTests(Class<?> c) {
256             List<Object[]> params2 = new ArrayList<>();
257             for ( Object x : tests.get(c) ) params2.add(new Object[]{x});
258             return params2.toArray(new Object[][]{});
259         }
260 
261         @Override
toString()262         public String toString() {
263             return "TestDataProvider("+name+")";
264         }
265     }
266 
267     /**
268      * Creates a temp file that will be deleted on exit after tests are complete.
269      *
270      * This will also mark the corresponding Tribble/Tabix/BAM indices matching the temp file for deletion.
271      * @param name Prefix of the file.
272      * @param extension Extension to concat to the end of the file.
273      * @return A file in the temporary directory starting with name, ending with extension, which will be deleted after the program exits.
274      */
createTempFile(final String name, final String extension)275     public static File createTempFile(final String name, final String extension) {
276         return IOUtils.createTempFile(name, extension);
277     }
278 
279     /**
280      * Creates a temp path that will be deleted on exit after tests are complete.
281      *
282      * This will also mark the corresponding Tribble/Tabix/BAM indices matching the temp file for deletion.
283      * @param name Prefix of the path.
284      * @param extension Extension to concat to the end of the path.
285      * @return A file in the temporary directory starting with name, ending with extension, which will be deleted after the program exits.
286      */
createTempPath(final String name, final String extension)287     public static Path createTempPath(final String name, final String extension) {
288         return IOUtils.createTempPath(name, extension);
289     }
290 
291     /**
292      * Return a File object representing a file with the given name and extension that is guaranteed not to exist.
293      * @param fileNameWithExtension
294      * @return File object representing a file that is guaranteed not to exist
295      */
getSafeNonExistentFile(final String fileNameWithExtension)296     public static File getSafeNonExistentFile(final String fileNameWithExtension) {
297         final File tempDir = createTempDir("nonExistentFileHolder");
298         final File nonExistingFile = new File(tempDir, fileNameWithExtension);
299         return nonExistingFile;
300     }
301 
302     /**
303      * Return a Path object representing a file with the given name and extension that is guaranteed not to exist.
304      * @param fileNameWithExtension
305      * @return Path object representing a file that is guaranteed not to exist
306      */
getSafeNonExistentPath(final String fileNameWithExtension)307     public static Path getSafeNonExistentPath(final String fileNameWithExtension) {
308         return getSafeNonExistentGATKPath(fileNameWithExtension).toPath();
309     }
310 
311     /**
312      * Return a GATKPath object representing a file with the given name and extension that is guaranteed not to exist.
313      * @param fileNameWithExtension
314      * @return Path object representing a file that is guaranteed not to exist
315      */
getSafeNonExistentGATKPath(final String fileNameWithExtension)316     public static GATKPath getSafeNonExistentGATKPath(final String fileNameWithExtension) {
317         final File tempDir = createTempDir("nonExistentFileHolder");
318         return new GATKPath(String.format("%s/%s", tempDir.getAbsolutePath(), fileNameWithExtension));
319     }
320 
321     /**
322      * Creates an empty temp directory which will be deleted on exit after tests are complete
323      *
324      * @param prefix prefix for the directory name
325      * @return an empty directory starting with prefix which will be deleted after the program exits
326      */
createTempDir(final String prefix)327     public static File createTempDir(final String prefix){
328         return IOUtils.createTempDir(prefix);
329     }
330 
331     /**
332      * Log this message so that it shows up inline during output as well as in html reports
333      */
log(final String message)334     public static void log(final String message) {
335         Reporter.log(message, true);
336     }
337 
338     private static final double DEFAULT_FLOAT_TOLERANCE = 1e-1;
339 
340 
341     /**
342      * Checks whether two double array contain the same values or not.
343      * @param actual actual produced array.
344      * @param expected expected array.
345      * @param tolerance maximum difference between double value to be consider equivalent.
346      */
assertEqualsDoubleArray(final double[] actual, final double[] expected, final double tolerance)347     protected static void assertEqualsDoubleArray(final double[] actual, final double[] expected, final double tolerance) {
348         if (expected == null)
349             Assert.assertNull(actual);
350         else {
351             Assert.assertNotNull(actual);
352             Assert.assertEquals(actual.length,expected.length,"array length");
353         }
354         for (int i = 0; i < actual.length; i++)
355             Assert.assertEquals(actual[i],expected[i],tolerance,"array position " + i);
356     }
357 
assertEqualsDoubleSmart(final Object actual, final Double expected, final double tolerance)358     public static void assertEqualsDoubleSmart(final Object actual, final Double expected, final double tolerance) {
359         Assert.assertTrue(actual instanceof Double, "Not a double");
360         assertEqualsDoubleSmart((double) (Double) actual, (double) expected, tolerance);
361     }
362 
assertEqualsDoubleSmart(final double actual, final double expected)363     public static void assertEqualsDoubleSmart(final double actual, final double expected) {
364         assertEqualsDoubleSmart(actual, expected, DEFAULT_FLOAT_TOLERANCE);
365     }
366 
assertEqualsSet(final Set<T> actual, final Set<T> expected, final String info)367     public static <T> void assertEqualsSet(final Set<T> actual, final Set<T> expected, final String info) {
368         final Set<T> actualSet = new LinkedHashSet<>(actual);
369         final Set<T> expectedSet = new LinkedHashSet<>(expected);
370         Assert.assertTrue(actualSet.equals(expectedSet), info); // note this is necessary due to testng bug for set comps
371     }
372 
assertEqualsDoubleSmart(final double actual, final double expected, final double tolerance)373     public static void assertEqualsDoubleSmart(final double actual, final double expected, final double tolerance) {
374         assertEqualsDoubleSmart(actual, expected, tolerance, null);
375     }
376 
assertEqualsDoubleSmart(final double actual, final double expected, final double tolerance, final String message)377     public static void assertEqualsDoubleSmart(final double actual, final double expected, final double tolerance, final String message) {
378         if ( Double.isNaN(expected) ) // NaN == NaN => false unfortunately
379             Assert.assertTrue(Double.isNaN(actual), "expected is nan, actual is not");
380         else if ( Double.isInfinite(expected) ) // NaN == NaN => false unfortunately
381             Assert.assertTrue(Double.isInfinite(actual), "expected is infinite, actual is not");
382         else {
383             final double delta = Math.abs(actual - expected);
384             final double ratio = Math.abs(actual / expected - 1.0);
385             Assert.assertTrue(delta < tolerance || ratio < tolerance, "expected = " + expected + " actual = " + actual
386                     + " not within tolerance " + tolerance
387                     + (message == null ? "" : "message: " + message));
388         }
389     }
390 
391     public static void assertEqualsIntSmart(final int actual, final int expected, final int tolerance, final String message) {
392         final double delta = Math.abs(actual - expected);
393         final double ratio = Math.abs(actual / expected - 1.0);
394         Assert.assertTrue(delta < tolerance || ratio < tolerance, "expected = " + expected + " actual = " + actual
395                 + " not within tolerance " + tolerance
396                 + (message == null ? "" : "message: " + message));
397     }
398 
399 
400     /**
401      * captures {@link System#out} while runnable is executing
402      * @param runnable a code block to execute
403      * @return everything written to {@link System#out} by runnable
404      */
405     public static String captureStdout(Runnable runnable){
406         return captureSystemStream(runnable, System.out, System::setOut);
407     }
408 
409 
410     /**
411      * captures {@link System#err} while runnable is executing
412      * @param runnable a code block to execute
413      * @return everything written to {@link System#err} by runnable
414      */
415     public static String captureStderr(Runnable runnable){
416         return captureSystemStream(runnable, System.err, System::setErr);
417     }
418 
419     private static String captureSystemStream(Runnable runnable,  PrintStream stream, Consumer<? super PrintStream> setter){
420         ByteArrayOutputStream out = new ByteArrayOutputStream();
421         setter.accept(new PrintStream(out));
422         try {
423             runnable.run();
424         } finally{
425             setter.accept(stream);
426         }
427         return out.toString();
428     }
429 
430     public static void assertContains(String actual, String expectedSubstring){
431         Assert.assertTrue(actual.contains(expectedSubstring),  expectedSubstring +" was not found in " + actual + ".");
432     }
433 
434     public static <T> void assertCondition(Iterable<T> actual, Iterable<T> expected, BiConsumer<T,T> assertion){
435         final Iterator<T> iterActual = actual.iterator();
436         final Iterator<T> iterExpected = expected.iterator();
437         while(iterActual.hasNext() && iterExpected.hasNext()){
438             assertion.accept(iterActual.next(), iterExpected.next());
439         }
440         if (iterActual.hasNext()){
441             Assert.fail("actual is longer than expected with at least one additional element: " + iterActual.next());
442         }
443         if (iterExpected.hasNext()){
444             Assert.fail("actual is shorter than expected, missing at least one element: " + iterExpected.next());
445         }
446     }
447 
448     /**
449      * assert that the iterable is sorted according to the comparator
450      */
451     public static <T> void assertSorted(Iterable<T> iterable, Comparator<T> comparator){
452         final Iterator<T> iter = iterable.iterator();
453         assertSorted(iter, comparator);
454     }
455 
456     /**
457      * assert that the iterator is sorted according to the comparator
458      */
459     public static <T> void assertSorted(Iterator<T> iterator, Comparator<T> comparator){
460         assertSorted(iterator, comparator, null);
461     }
462 
463 
464     /**
465      * assert that the iterator is sorted according to the comparator
466      */
467     public static <T> void assertSorted(Iterable<T> iterable, Comparator<T> comparator, String message){
468        assertSorted(iterable.iterator(), comparator, message);
469     }
470 
471 
472     /**
473      * assert that the iterator is sorted according to the comparator
474      */
475     public static <T> void assertSorted(Iterator<T> iterator, Comparator<T> comparator, String message){
476         T previous = null;
477         while(iterator.hasNext()){
478             T current = iterator.next();
479             if( previous != null) {
480                 Assert.assertTrue(comparator.compare(previous, current) <= 0, "Expected " + previous + " to be <= " + current + (message == null ? "" : "\n"+message));
481             }
482             previous = current;
483         }
484     }
485 
486     /**
487      * Get a FileSystem that uses the explicit credentials instead of the default
488      * credentials.
489      *
490      * @param bucket the name of the bucket to access.
491      *
492      * @return A FileSystem for that bucket on GCS, using explicit credentials.
493      *
494      * @throws IOException
495      */
496     protected FileSystem getAuthenticatedGcs(final String bucket) throws IOException {
497         final byte[] creds = Files.readAllBytes(Paths.get(getGoogleServiceAccountKeyPath()));
498         return BucketUtils.getAuthenticatedGcs(getGCPTestProject(), bucket, creds);
499     }
500 
501     /**
502      * Print information to the console that helps understand which credentials are in use
503      * (are they the ones you meant to use?).
504      */
505     protected void helpDebugAuthError() {
506         final String key = "GOOGLE_APPLICATION_CREDENTIALS";
507         String credsFile = System.getenv(key);
508         if (null == credsFile) {
509             System.err.println("$" + key + " is not defined.");
510             return;
511         }
512         System.err.println("$" + key + " = " + credsFile);
513         Path credsPath = Paths.get(credsFile);
514         boolean exists = Files.exists(credsPath);
515         System.err.println("File exists: " + exists);
516         if (exists) {
517             try {
518                 System.err.println("Key lines from file:");
519                 printKeyLines(credsPath, "\"type\"", "\"project_id\"", "\"client_email\"");
520             } catch (IOException x2) {
521                 System.err.println("Unable to read: " + x2.getMessage());
522             }
523         }
524     }
525 
526     private void printKeyLines(Path path, String... keywords) throws IOException {
527         for (String line : Files.readAllLines(path)) {
528             for (String keyword : keywords) {
529                 if (line.contains(keyword)) {
530                     System.err.println(line);
531                 }
532             }
533         }
534     }
535 }
536 
537