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