diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 1c2d1ad..1096efc 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -3,14 +3,23 @@ Please keep the list sorted alphabetically by handle ----- - | Github ID | Name | Email | |----------------------------------------------|------------------------|------------------------------------| -| [@jacarey](https://github.com/jacarey) | Jay Carey | | +| [@cmnbroad](https://github.com/cmnbroad) | Chris Norman | | | [@lbergelson](https://github.com/lbergelson) | Louis Bergelson | | | [@magicDGS](https://github.com/magicDGS) | Daniel Gómez Sánchez | | | [@tfenne](https://github.com/tfenne) | Tim Fennell | | ---- +### These are prior project maintainers + +----- + +| Github ID | Name | Email | +|----------------------------------------------|------------------------|------------------------------------| +| [@jacarey](https://github.com/jacarey) | Jay Carey | | + +---- + All of the current maintainers can be contacted by mentioning @samtools/htsjdk-next-maintainers diff --git a/appveyor.yml b/appveyor.yml index 692246b..fcf2cd3 100755 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,10 +3,6 @@ version: '{branch}.{build}' pull_requests: do_not_increment_build_number: true -branches: - only: - - master - environment: matrix: - JAVA_HOME: C:\Program Files\Java\jdk1.8.0 diff --git a/build.gradle b/build.gradle index 3423174..ab434bc 100644 --- a/build.gradle +++ b/build.gradle @@ -8,6 +8,8 @@ buildscript { plugins { id 'com.github.johnrengelman.shadow' version '2.0.4' id 'java-library' + id 'scala' + id 'com.github.maiflai.scalatest' version '0.21' } //This allows you to build a single shadowJar with the contents of all the @@ -18,6 +20,8 @@ dependencies { subprojects { apply plugin: 'java-library' + apply plugin: 'scala' + apply plugin: 'com.github.maiflai.scalatest' group 'org.htsjdk' version '0.0.1' @@ -33,13 +37,12 @@ subprojects { testCompile "com.google.jimfs:jimfs:1.1" testCompile "org.apache.commons:commons-lang3:3.7" - compile 'commons-io:commons-io:2.5' - } + testCompile "org.scala-lang:scala-library:2.12.4" + testCompile 'org.scalatest:scalatest_2.12:3.0.5' + testRuntime 'org.pegdown:pegdown:1.4.2' - test { - useTestNG{} + compile 'commons-io:commons-io:2.5' } - } project(':cram') { diff --git a/core/- b/core/- deleted file mode 100644 index 9dfbf8b..0000000 --- a/core/- +++ /dev/null @@ -1 +0,0 @@ -some stuff \ No newline at end of file diff --git a/core/out/production/resources/META-INF/services/org.htsjdk.core.api.HtsjdkCodecDescriptor b/core/out/production/resources/META-INF/services/org.htsjdk.core.api.HtsjdkCodecDescriptor new file mode 100644 index 0000000..53447ab --- /dev/null +++ b/core/out/production/resources/META-INF/services/org.htsjdk.core.api.HtsjdkCodecDescriptor @@ -0,0 +1,2 @@ +org.htsjdk.core.impl.bam.BAMCodecDescriptor + diff --git a/core/src/main/java/org/htsjdk/core/api/HtsjdkCodec.java b/core/src/main/java/org/htsjdk/core/api/HtsjdkCodec.java new file mode 100644 index 0000000..23b72a4 --- /dev/null +++ b/core/src/main/java/org/htsjdk/core/api/HtsjdkCodec.java @@ -0,0 +1,13 @@ +package org.htsjdk.core.api; + +import org.htsjdk.core.api.io.IOResource; +import org.htsjdk.core.utils.Version; + +import java.nio.file.Path; + +/** + * Base interface that must be implemented by all htsjdk codecs + */ +public interface HtsjdkCodec extends Upgradeable { + +} diff --git a/core/src/main/java/org/htsjdk/core/api/HtsjdkCodecDescriptor.java b/core/src/main/java/org/htsjdk/core/api/HtsjdkCodecDescriptor.java new file mode 100644 index 0000000..36c68ac --- /dev/null +++ b/core/src/main/java/org/htsjdk/core/api/HtsjdkCodecDescriptor.java @@ -0,0 +1,39 @@ +package org.htsjdk.core.api; + +import org.htsjdk.core.api.io.IOResource; +import org.htsjdk.core.utils.PathSpecifier; + +import java.io.InputStream; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +/** + * Base interface that must be implemented by all htsjdk codec descriptors. Descriptors + * are lightweight object that are cached by the registry service and used to locate + * and instantiate a codec for a given input. + */ +public interface HtsjdkCodecDescriptor { + + String getName(); + + // Get the minimum number of bytes this codec needs in order to determine whether it can decode a stream. + int getMinimalFileSignatureSize(); + + boolean canDecode(final IOResource resource); + + boolean canDecode(final Path path); + + HtsjdkCodec getCodecInstance(final IOResource resource); + + HtsjdkCodec getCodecInstance(final Path path); + + HtsjdkCodec getCodecInstance(final InputStream is); + + boolean canDecodeSignature(final byte[] streamSignature); + + //Given a pathSpec, resolve any companion sibling files against it (index, .dict, etc.) + default List resolveSiblings(final PathSpecifier pathSpec) { + return Collections.singletonList(pathSpec); + } +} diff --git a/core/src/main/java/org/htsjdk/core/api/Upgradeable.java b/core/src/main/java/org/htsjdk/core/api/Upgradeable.java new file mode 100644 index 0000000..bdd1b15 --- /dev/null +++ b/core/src/main/java/org/htsjdk/core/api/Upgradeable.java @@ -0,0 +1,9 @@ +package org.htsjdk.core.api; + +import org.htsjdk.core.utils.Version; + +public interface Upgradeable { + + boolean runVersionUpgrade(final Version sourceVersion, final Version targetVersion); + +} diff --git a/core/src/main/java/org/htsjdk/core/exception/HtsjdkPluginException.java b/core/src/main/java/org/htsjdk/core/exception/HtsjdkPluginException.java new file mode 100644 index 0000000..869bd48 --- /dev/null +++ b/core/src/main/java/org/htsjdk/core/exception/HtsjdkPluginException.java @@ -0,0 +1,16 @@ +package org.htsjdk.core.exception; + +/** + * Base class for exceptions resulting from ill-behaved codec plugins + */ +public class HtsjdkPluginException extends HtsjdkException { + /** + * Constructs an HTSJDK exception. + * + * @param message detailed message. + */ + public HtsjdkPluginException(String message) { + super(message); + } + +} diff --git a/core/src/main/java/org/htsjdk/core/impl/bam/BAMCodec.java b/core/src/main/java/org/htsjdk/core/impl/bam/BAMCodec.java new file mode 100644 index 0000000..c05afdd --- /dev/null +++ b/core/src/main/java/org/htsjdk/core/impl/bam/BAMCodec.java @@ -0,0 +1,44 @@ +package org.htsjdk.core.impl.bam; + +import org.htsjdk.core.api.HtsjdkCodec; +import org.htsjdk.core.api.io.IOResource; +import org.htsjdk.core.utils.Version; + +import java.io.InputStream; +import java.nio.file.Path; + +/** + * Mock, do-nothing BAM codec used to exercise the reader factory infrastructure + */ +public class BAMCodec implements HtsjdkCodec { + + public BAMCodec(final IOResource ioResource) { this(ioResource.toPath()); } + + public BAMCodec(final Path pathResource) { + // TODO + } + + public BAMCodec(final InputStream inputStream) { + + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(final Object obj) { + return super.equals(obj); + } + + @Override + public String toString() { + return super.toString(); + } + + @Override + public boolean runVersionUpgrade(final Version sourceVersion, final Version targetVersion) { + return false; + } +} diff --git a/core/src/main/java/org/htsjdk/core/impl/bam/BAMCodecDescriptor.java b/core/src/main/java/org/htsjdk/core/impl/bam/BAMCodecDescriptor.java new file mode 100644 index 0000000..8c553d1 --- /dev/null +++ b/core/src/main/java/org/htsjdk/core/impl/bam/BAMCodecDescriptor.java @@ -0,0 +1,82 @@ +package org.htsjdk.core.impl.bam; + +import org.htsjdk.core.api.HtsjdkCodecDescriptor; +import org.htsjdk.core.api.HtsjdkCodec; +import org.htsjdk.core.api.io.IOResource; +import org.htsjdk.core.exception.HtsjdkIOException; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Mock, do-nothing BAM codec descriptor. + */ +public class BAMCodecDescriptor implements HtsjdkCodecDescriptor { + + public static final String BAM_FILE_EXTENSION = ".bam"; + public static final String BAM_MAGIC = "BAM\1"; + + @Override + public String getName() { + return "BAM codec descriptor"; + } + + @Override + public int getMinimalFileSignatureSize() { + return BAM_MAGIC.length(); + } + + @Override + public boolean canDecode(final IOResource resource) { + return resource.getURIString().endsWith(BAM_FILE_EXTENSION); + } + + @Override + public boolean canDecode(final Path path) { + return path.endsWith(BAM_FILE_EXTENSION); + } + + // uses a byte array rather than a stream to reduce the need to repeatedly mark/reset the + // stream for each codec + @Override + public boolean canDecodeSignature(final byte[] signatureBytes) { + return signatureBytes.equals("BAM"); + } + + @Override + public HtsjdkCodec getCodecInstance(final IOResource ioResource) { + return getCodecInstance(ioResource.toPath()); + } + + @Override + public HtsjdkCodec getCodecInstance(final Path resourcePath) { + try { + return new BAMCodec(Files.newInputStream(resourcePath)); + } catch (IOException e) { + throw new HtsjdkIOException(e); + } + } + + @Override + public HtsjdkCodec getCodecInstance(final InputStream is) { + return new BAMCodec(is); + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(final Object obj) { + return super.equals(obj); + } + + @Override + public String toString() { + return super.toString(); + } + +} diff --git a/core/src/main/java/org/htsjdk/core/utils/CodecDiscoveryService.java b/core/src/main/java/org/htsjdk/core/utils/CodecDiscoveryService.java new file mode 100644 index 0000000..2020c8a --- /dev/null +++ b/core/src/main/java/org/htsjdk/core/utils/CodecDiscoveryService.java @@ -0,0 +1,24 @@ +package org.htsjdk.core.utils; + +import org.htsjdk.core.api.HtsjdkCodecDescriptor; + +import java.util.*; + +/** + * Service loader for dynamically discovering htsjdk codecs. + */ +public class CodecDiscoveryService { + private static ServiceLoader serviceLoader = ServiceLoader.load(HtsjdkCodecDescriptor.class); + + private CodecDiscoveryService() {} + + public static List discoverCodecs() { + final List descriptors = new ArrayList<>(); + + final Iterator descriptorIterator = serviceLoader.iterator(); + while (descriptorIterator.hasNext()) { + descriptors.add(descriptorIterator.next()); + } + return descriptors; + } +} \ No newline at end of file diff --git a/core/src/main/java/org/htsjdk/core/utils/CodecRegistry.java b/core/src/main/java/org/htsjdk/core/utils/CodecRegistry.java new file mode 100644 index 0000000..865fc96 --- /dev/null +++ b/core/src/main/java/org/htsjdk/core/utils/CodecRegistry.java @@ -0,0 +1,83 @@ +package org.htsjdk.core.utils; + +import org.htsjdk.core.api.HtsjdkCodec; +import org.htsjdk.core.api.HtsjdkCodecDescriptor; +import org.htsjdk.core.api.io.IOResource; +import org.htsjdk.core.exception.HtsjdkIOException; +import org.htsjdk.core.exception.HtsjdkPluginException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.*; + +/** + * Registry/cache for codec descriptors for discovered codecs. + */ +public class CodecRegistry { + private static final CodecRegistry codecRegistry = new CodecRegistry(); + + private static List discoveredCodecs = new ArrayList<>(); + + // minimum number of bytes required to allow any codec to deterministically decide if it can + // decode a stream + private static int minSignatureSize = 0; + + static { + CodecDiscoveryService.discoverCodecs().forEach(codecRegistry::addCodecDescriptor); + } + + /** + * Add a codec to the registry + */ + private void addCodecDescriptor(final HtsjdkCodecDescriptor codecDescriptor) { + discoveredCodecs.add(codecDescriptor); + final int minSignatureBytesRequired = codecDescriptor.getMinimalFileSignatureSize(); + if (minSignatureBytesRequired < 1) { + throw new HtsjdkPluginException( + String.format("%s: getMinimalFileSignatureSize must be > 0", codecDescriptor.getName()) + ); + } + minSignatureSize = Integer.max(minSignatureSize, minSignatureBytesRequired); + } + + // TODO: this should have a name and contract that reflects that its only looking at the URI + // Once we find a codec, hand it off already primed with the version header, etc). + public static HtsjdkCodec findCodecFor(final IOResource inputResource) { + final Optional codec = + discoveredCodecs.stream() + .filter(codecDescriptor -> codecDescriptor.canDecode(inputResource)) + .findFirst(); + + if (codec.isPresent()) { + return codec.get().getCodecInstance(inputResource); + } else { + //TODO: who closes/owns the lifetime of this input stream ? + InputStream is = inputResource.getInputStream(); + return findCodecFor(inputResource.toString(), is); + } + } + + // Once we find a codec, hand it off already primed with the version header, etc). + public static HtsjdkCodec findCodecFor(final String sourceName, final InputStream is) { + final byte[] signatureBytes = new byte[minSignatureSize]; + try { + final int numRead = is.read(signatureBytes); + if (numRead <= 0) { + throw new HtsjdkIOException(String.format("Failure reading content from stream for %s", sourceName)); + } + return discoveredCodecs.stream() + .filter( + // its possible that the input is a legitimate stream for some codec, but + // contains less bytes than are required even for signature detection by another + // codecs, so skip any descriptors that require more bytes than are available + codecDescriptor -> + numRead >= codecDescriptor.getMinimalFileSignatureSize() && + codecDescriptor.canDecodeSignature(signatureBytes)) + .findFirst() + .orElseThrow(() -> new HtsjdkIOException(String.format("No codec found for %s", sourceName))) + .getCodecInstance(is); + } catch (IOException e) { + throw new HtsjdkIOException(String.format("Failure reading signature from stream for %s", sourceName), e); + } + } +} diff --git a/core/src/main/java/org/htsjdk/core/utils/Version.java b/core/src/main/java/org/htsjdk/core/utils/Version.java new file mode 100644 index 0000000..fc5b91d --- /dev/null +++ b/core/src/main/java/org/htsjdk/core/utils/Version.java @@ -0,0 +1,69 @@ +package org.htsjdk.core.utils; + +public class Version { + + private final String formatString = "%d.%d.%d"; + + private final int majorVersion; + private final int minorVersion; + private final int patchVersion; + + public Version(final int major, final int minor, final int patch) { + this.majorVersion = major; + this.minorVersion = minor; + this.patchVersion = patch; + } + + public Version(final String versionString) { + ParamUtils.nonNull(versionString); + final String[] parts = versionString.split(".", 0); + if (parts.length != 3) { + throw new IllegalArgumentException(String.format("Can parse version string: '%s'", versionString)); + } + try { + majorVersion = Integer.parseInt(parts[0]); + minorVersion = Integer.parseInt(parts[1]); + patchVersion = Integer.parseInt(parts[2]); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(String.format("Can parse version string: '%s'", versionString)); + } + + } + + public int getMajorVersion() { + return majorVersion; + } + + public int getMinorVersion() { + return minorVersion; + } + + public int getPatchVersion() { + return patchVersion; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Version)) return false; + + Version version = (Version) o; + + if (getMajorVersion() != version.getMajorVersion()) return false; + if (getMinorVersion() != version.getMinorVersion()) return false; + return getPatchVersion() == version.getPatchVersion(); + } + + @Override + public int hashCode() { + int result = getMajorVersion(); + result = 31 * result + getMinorVersion(); + result = 31 * result + getPatchVersion(); + return result; + } + + @Override + public String toString() { + return String.format(formatString, getMajorVersion(), getMinorVersion(), getPatchVersion()); + } +} diff --git a/core/src/main/java/org/htsjdk/core/utils/VersionUtils.java b/core/src/main/java/org/htsjdk/core/utils/VersionUtils.java new file mode 100644 index 0000000..b098521 --- /dev/null +++ b/core/src/main/java/org/htsjdk/core/utils/VersionUtils.java @@ -0,0 +1,10 @@ +package org.htsjdk.core.utils; + +public class VersionUtils { + + public boolean runVersionUpgrade(final Version sourceVersion, final Version targetVersion) { + + // run upgrade chain + return true; + } +} diff --git a/core/src/main/java/org/htsjdk/test/HtsjdkBaseTest.java b/core/src/main/java/org/htsjdk/test/HtsjdkBaseTest.java index bbb7bb8..16be586 100644 --- a/core/src/main/java/org/htsjdk/test/HtsjdkBaseTest.java +++ b/core/src/main/java/org/htsjdk/test/HtsjdkBaseTest.java @@ -1,7 +1,14 @@ package org.htsjdk.test; +import java.nio.file.Path; +import java.nio.file.Paths; + /** * All tests should extend from this base class. */ public class HtsjdkBaseTest { + + public final Path getDataRootPath() { + return Paths.get(Paths.get("../data").toUri().normalize().getPath()); + } } diff --git a/core/src/main/resources/META-INF/services/org.htsjdk.core.api.HtsjdkCodecDescriptor b/core/src/main/resources/META-INF/services/org.htsjdk.core.api.HtsjdkCodecDescriptor new file mode 100644 index 0000000..53447ab --- /dev/null +++ b/core/src/main/resources/META-INF/services/org.htsjdk.core.api.HtsjdkCodecDescriptor @@ -0,0 +1,2 @@ +org.htsjdk.core.impl.bam.BAMCodecDescriptor + diff --git a/core/src/test/java/org/htsjdk/core/utils/CodecDiscoveryServiceTest.java b/core/src/test/java/org/htsjdk/core/utils/CodecDiscoveryServiceTest.java new file mode 100644 index 0000000..9ebb2b9 --- /dev/null +++ b/core/src/test/java/org/htsjdk/core/utils/CodecDiscoveryServiceTest.java @@ -0,0 +1,18 @@ +package org.htsjdk.core.utils; + +import org.htsjdk.core.api.HtsjdkCodecDescriptor; +import org.htsjdk.core.utils.CodecDiscoveryService; +import org.htsjdk.test.HtsjdkBaseTest; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.List; + +public class CodecDiscoveryServiceTest extends HtsjdkBaseTest { + + @Test + public void testDescriptorServiceLoader() { + final List descriptors = CodecDiscoveryService.discoverCodecs(); + Assert.assertFalse(descriptors.isEmpty()); + } +} diff --git a/core/src/test/java/org/htsjdk/core/utils/CodecRegistryTest.java b/core/src/test/java/org/htsjdk/core/utils/CodecRegistryTest.java new file mode 100644 index 0000000..0bd7a3f --- /dev/null +++ b/core/src/test/java/org/htsjdk/core/utils/CodecRegistryTest.java @@ -0,0 +1,19 @@ +package org.htsjdk.core.utils; + +import org.htsjdk.core.api.HtsjdkCodec; +import org.htsjdk.core.api.io.IOResource; +import org.htsjdk.test.HtsjdkBaseTest; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.nio.file.Paths; + +public class CodecRegistryTest extends HtsjdkBaseTest { + + @Test + public void testFindCodec() { + final IOResource ioResource = new PathSpecifier(getDataRootPath().resolve(Paths.get("simple.bam")).toString()); + HtsjdkCodec codec = CodecRegistry.findCodecFor(ioResource); + Assert.assertNotNull(codec); + } +} diff --git a/core/src/test/java/org/htsjdk/core/utils/PathSpecifierUnitTest.java b/core/src/test/java/org/htsjdk/core/utils/PathSpecifierUnitTest.java deleted file mode 100644 index 4905d97..0000000 --- a/core/src/test/java/org/htsjdk/core/utils/PathSpecifierUnitTest.java +++ /dev/null @@ -1,358 +0,0 @@ -package org.htsjdk.core.utils; - -//TODO: NAMING: is there any useful distinction between Unit/Integration tests in this repo ? - -import com.google.common.jimfs.Configuration; -import com.google.common.jimfs.Jimfs; -import org.apache.commons.lang3.SystemUtils; -import org.htsjdk.core.api.io.IOResource; -import org.testng.Assert; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -import java.io.*; -import java.nio.file.*; - -public class PathSpecifierUnitTest { - - final static String FS_SEPARATOR = FileSystems.getDefault().getSeparator(); - - @DataProvider - public Object[][] validPathSpecifiers() { - return new Object[][] { - // Paths specifiers that are syntactically valid as either a relative or absolute local file - // name, or as a URI, but which may fail isNIO or isPath - - // input String, expected resulting URI String, expected isNIO, expected isPath - - //******************************** - // Local (non-URI) file references - //******************************** - - {"localFile.bam", "file://" + getCWDAsURIPathString() + "localFile.bam", true, true}, - // absolute reference to a file in the root of the current file system (Windows accepts the "/" as root) - {"/localFile.bam", "file://" + getRootDirectoryAsURIPathString() + "localFile.bam", true, true}, - // absolute reference to a file in the root of the current file system, where root is specified using the - // default FS separator - {FS_SEPARATOR + "localFile.bam", "file://" + getRootDirectoryAsURIPathString() + "localFile.bam", true, true}, - // absolute reference to a file - {FS_SEPARATOR + joinWithFSSeparator("path", "to", "localFile.bam"), - "file://" + getRootDirectoryAsURIPathString() + "path/to/localFile.bam", true, true}, - // absolute reference to a file that contains a URI "excluded" character in the path ("#"), which without - // encoding will be treated as a fragment delimiter - {FS_SEPARATOR + joinWithFSSeparator("project", "gvcf-pcr", "23232_1#1", "1.g.vcf.gz"), - "file://" + getRootDirectoryAsURIPathString() + "project/gvcf-pcr/23232_1%231/1.g.vcf.gz", true, true}, - // relative reference to a file on the local file system - {joinWithFSSeparator("path", "to", "localFile.bam"), - "file://" + getCWDAsURIPathString() + "path/to/localFile.bam", true, true}, - // Windows also accepts "/" as a valid root specifier - {"/", "file://" + getRootDirectoryAsURIPathString(), true, true}, - {".", "file://" + getCWDAsURIPathString() + "./", true, true}, - {"../.", "file://" + getCWDAsURIPathString() + ".././", true, true}, - // an empty path is equivalent to accessing the current directory of the default file system - {"", "file://" + getCWDAsURIPathString(), true, true}, - - //*********************************************************** - // Local file references using a URI with a "file://" scheme. - //*********************************************************** - - {"file:localFile.bam", "file:localFile.bam", true, false}, // absolute, opaque (not hierarchical) - {"file:/localFile.bam", "file:/localFile.bam", true, true}, // absolute, hierarchical - {"file://localFile.bam", "file://localFile.bam", true, false}, // file URLs can't have an authority ("localFile.bam") - {"file:///localFile.bam", "file:///localFile.bam", true, true}, // empty authority - {"file:path/to/localFile.bam", "file:path/to/localFile.bam", true, false}, - {"file:/path/to/localFile.bam", "file:/path/to/localFile.bam", true, true}, - // "path" appears to be an authority, and will be accepted on Windows since this URI will be - // interpreted as a UNC path containing an authority - {"file://path/to/localFile.bam", "file://path/to/localFile.bam", true, SystemUtils.IS_OS_WINDOWS}, - // "localhost" is accepted as a special case authority for "file://" Paths on Windows; but not Linux/Mac - {"file://localhost/to/localFile.bam","file://localhost/to/localFile.bam", true, SystemUtils.IS_OS_WINDOWS}, - {"file:///path/to/localFile.bam", "file:///path/to/localFile.bam", true, true}, // empty authority - - //***************************************************************************** - // Valid URIs which are NOT valid NIO paths (no installed file system provider) - //***************************************************************************** - - {"gs://file.bam", "gs://file.bam", false, false}, - {"gs://bucket/file.bam", "gs://bucket/file.bam", false, false}, - {"gs:///bucket/file.bam", "gs:///bucket/file.bam", false, false}, - {"gs://auth/bucket/file.bam", "gs://auth/bucket/file.bam", false, false}, - {"gs://hellbender/test/resources/", "gs://hellbender/test/resources/", false, false}, - {"gcs://abucket/bucket", "gcs://abucket/bucket", false, false}, - {"gendb://somegdb", "gendb://somegdb", false, false}, - {"chr1:1-100", "chr1:1-100", false, false}, - - //***************************************************************************************** - // Valid URIs which are backed by an installed NIO file system provider), but are which not - // actually resolvable as paths because the scheme-specific part is not valid for one reason - // or another. - //********************************************************************************************** - - // uri must have a path: jimfs:file.bam - {"jimfs:file.bam", "jimfs:file.bam", true, false}, - // java.lang.AssertionError: java.net.URISyntaxException: Expected scheme-specific part at index 6: jimfs: - {"jimfs:/file.bam", "jimfs:/file.bam", true, false}, - // java.lang.AssertionError: uri must have a path: jimfs://file.bam - {"jimfs://file.bam", "jimfs://file.bam", true, false}, - // java.lang.AssertionError: java.net.URISyntaxException: Expected scheme-specific part at index 6: jimfs: - {"jimfs:///file.bam", "jimfs:///file.bam", true, false}, - // java.nio.file.FileSystemNotFoundException: jimfs://root - {"jimfs://root/file.bam","jimfs://root/file.bam", true, false}, - - //*********************************************************************************************** - // References that contain characters that require URI-encoding. If the input string is presented - // without no scheme, it will be be automatically encoded by PathSpecifier, otherwise it - // must already be URI-encoded. - //*********************************************************************************************** - - // relative (non-URI) reference to a file on the local file system that contains a URI fragment delimiter - // is automatically URI-encoded - {joinWithFSSeparator("project", "gvcf-pcr", "23232_1#1", "1.g.vcf.gz"), - "file://" + getCWDAsURIPathString() + "project/gvcf-pcr/23232_1%231/1.g.vcf.gz", true, true}, - // URI reference with fragment delimiter is not automatically URI-encoded - {"file:project/gvcf-pcr/23232_1#1/1.g.vcf.gz", "file:project/gvcf-pcr/23232_1#1/1.g.vcf.gz", true, false}, - {"file:/project/gvcf-pcr/23232_1#1/1.g.vcf.gz", "file:/project/gvcf-pcr/23232_1#1/1.g.vcf.gz", true, false}, - {"file:///project/gvcf-pcr/23232_1%231/1.g.vcf.g", "file:///project/gvcf-pcr/23232_1%231/1.g.vcf.g", true, true}, - }; - } - - @Test(dataProvider = "validPathSpecifiers") - public void testPathSpecifier(final String referenceString, final String expectedURIString, final boolean isNIO, final boolean isPath) { - final IOResource ioResource = new PathSpecifier(referenceString); - Assert.assertNotNull(ioResource); - Assert.assertEquals(ioResource.getURI().toString(), expectedURIString); - } - - @Test(dataProvider = "validPathSpecifiers") - public void testIsNIO(final String referenceString, final String expectedURIString, final boolean isNIO, final boolean isPath) { - final IOResource pathURI = new PathSpecifier(referenceString); - Assert.assertEquals(pathURI.isNIO(), isNIO); - } - - @Test(dataProvider = "validPathSpecifiers") - public void testIsPath(final String referenceString, final String expectedURIString, final boolean isNIO, final boolean isPath) { - final IOResource pathURI = new PathSpecifier(referenceString); - if (isPath) { - Assert.assertEquals(pathURI.isPath(), isPath, pathURI.getToPathFailureReason().orElse("no failure")); - } else { - Assert.assertEquals(pathURI.isPath(), isPath); - } - } - - @Test(dataProvider = "validPathSpecifiers") - public void testToPath(final String referenceString, final String expectedURIString, final boolean isNIO, final boolean isPath) { - final IOResource pathURI = new PathSpecifier(referenceString); - if (isPath) { - final Path path = pathURI.toPath(); - Assert.assertEquals(path != null, isPath, pathURI.getToPathFailureReason().orElse("no failure")); - } else { - Assert.assertEquals(pathURI.isPath(), isPath); - } - } - - @DataProvider - public Object[][] invalidPathSpecifiers() { - return new Object[][] { - // the nul character is rejected on all of the supported platforms in both local - // filenames and URIs, so use it to test PathSpecifier constructor failure on all platforms - {"\0"}, - }; - } - - @Test(dataProvider = "invalidPathSpecifiers", expectedExceptions = {IllegalArgumentException.class}) - public void testPathSpecifierInvalid(final String referenceString) { - new PathSpecifier(referenceString); - } - - @DataProvider - public Object[][] invalidPath() { - return new Object[][] { - // valid references that are not valid as a path - - {"file:/project/gvcf-pcr/23232_1#1/1.g.vcf.gz"}, // not encoded - {"file:project/gvcf-pcr/23232_1#1/1.g.vcf.gz"}, // scheme-specific part is not hierarchical - - // The hadoop file system provider explicitly throws an NPE if no host is specified and HDFS is not - // the default file system - //{"hdfs://nonexistent_authority/path/to/file.bam"}, // unknown authority "nonexistent_authority" - {"hdfs://userinfo@host:80/path/to/file.bam"}, // UnknownHostException "host" - - {"unknownscheme://foobar"}, - {"gendb://adb"}, - {"gcs://abucket/bucket"}, - - // URIs with schemes that are backed by an valid NIO provider, but for which the - // scheme-specific part is not valid. - {"file://nonexistent_authority/path/to/file.bam"}, // unknown authority "nonexistent_authority" - }; - } - - @Test(dataProvider = "invalidPath") - public void testIsPathInvalid(final String invalidPathString) { - final IOResource htsURI = new PathSpecifier(invalidPathString); - Assert.assertFalse(htsURI.isPath()); - } - - @Test(dataProvider = "invalidPath", expectedExceptions = {IllegalArgumentException.class, FileSystemNotFoundException.class}) - public void testToPathInvalid(final String invalidPathString) { - final IOResource htsURI = new PathSpecifier(invalidPathString); - htsURI.toPath(); - } - - @Test - public void testInstalledNonDefaultFileSystem() throws IOException { - // create a jimfs file system and round trip through PathSpecifier/stream - try (FileSystem jimfs = Jimfs.newFileSystem(Configuration.unix())) { - final Path outputPath = jimfs.getPath("alternateFileSystemTest.txt"); - doStreamRoundTrip(outputPath.toUri().toString()); - } - } - - @DataProvider - public Object[][] inputStreamSpecifiers() throws IOException { - return new Object[][]{ - // references that can be resolved to an actual test file that can be read - - // relative (file) reference to a local file - {joinWithFSSeparator("..", "data", "utils", "testTextFile.txt"), "Test file."}, - - // absolute reference to a local file - {getCWDAsFileReference() + FS_SEPARATOR + joinWithFSSeparator("..", "data", "utils", "testTextFile.txt"), "Test file."}, - - // URI reference to a local file, where the path is absolute - {"file://" + getCWDAsURIPathString() + "../data/utils/testTextFile.txt", "Test file."}, - - // reference to a local file with an embedded fragment delimiter ("#") in the name; if the file - // scheme is included, the rest of the path must already be encoded; if no file scheme is - // included, the path is encoded by the PathSpecifier class - {joinWithFSSeparator("..", "data", "utils", "testDirWith#InName", "testTextFile.txt"), "Test file."}, - {"file://" + getCWDAsURIPathString() + "../data/utils/testDirWith%23InName/testTextFile.txt", "Test file."}, - }; - } - - @Test(dataProvider = "inputStreamSpecifiers") - public void testGetInputStream(final String referenceString, final String expectedFileContents) throws IOException { - final IOResource htsURI = new PathSpecifier(referenceString); - - try (final InputStream is = htsURI.getInputStream(); - final DataInputStream dis = new DataInputStream(is)) { - final byte[] actualFileContents = new byte[expectedFileContents.length()]; - dis.readFully(actualFileContents); - - Assert.assertEquals(new String(actualFileContents), expectedFileContents); - } - } - - @DataProvider - public Object[][] outputStreamSpecifiers() throws IOException { - return new Object[][]{ - // output URIs that can be resolved to an actual test file - {IOUtils.createTempPath("testOutputStream", ".txt").toString()}, - {"file://" + getLocalFileAsURIPathString(IOUtils.createTempPath("testOutputStream", ".txt"))}, - }; - } - - @Test(dataProvider = "outputStreamSpecifiers") - public void testGetOutputStream(final String referenceString) throws IOException { - doStreamRoundTrip(referenceString); - } - - @Test - public void testStdIn() throws IOException { - final IOResource htsURI = new PathSpecifier( - SystemUtils.IS_OS_WINDOWS ? - "-" : - "/dev/stdin"); - try (final InputStream is = htsURI.getInputStream(); - final DataInputStream dis = new DataInputStream(is)) { - final byte[] actualFileContents = new byte[0]; - dis.readFully(actualFileContents); - - Assert.assertEquals(new String(actualFileContents), ""); - } - } - - @Test - public void testStdOut() throws IOException { - final IOResource pathURI = new PathSpecifier( - SystemUtils.IS_OS_WINDOWS ? - "-" : - "/dev/stdout"); - try (final OutputStream os = pathURI.getOutputStream(); - final DataOutputStream dos = new DataOutputStream(os)) { - dos.write("some stuff".getBytes()); - } - } - - /** - * Return the string resulting from joining the individual components using the local default - * file system separator. - * - * This is used to create test inputs that are local file references, as would be presented by a - * user on the platform on which these tests are running. - */ - private String joinWithFSSeparator(String... parts) { - return String.join(FileSystems.getDefault().getSeparator(), parts); - } - - private void doStreamRoundTrip(final String referenceString) throws IOException { - final String expectedFileContents = "Test contents"; - - final IOResource pathURI = new PathSpecifier(referenceString); - try (final OutputStream os = pathURI.getOutputStream(); - final DataOutputStream dos = new DataOutputStream(os)) { - dos.write(expectedFileContents.getBytes()); - } - - // read it back in and make sure it matches expected contents - try (final InputStream is = pathURI.getInputStream(); - final DataInputStream dis = new DataInputStream(is)) { - final byte[] actualFileContents = new byte[expectedFileContents.length()]; - dis.readFully(actualFileContents); - - Assert.assertEquals(new String(actualFileContents), expectedFileContents); - } - } - - /** - * Get an absolute reference to the current working directory using local file system syntax and - * the local file system separator. Used to construct valid local, absolute file references as test inputs. - * - * Returns a string of the form '/some/path/.` or `d:\some\path\.` on Windows - */ - private static String getCWDAsFileReference() { - return new File(".").getAbsolutePath(); - } - - /** - * Get the current working directory as a locally valid, hierarchical URI string. Used to - * construct expected URI string values for test inputs that are local file references. - * - * Returns '/some/path/` or `/d:/some/path/` on Windows - */ - private String getCWDAsURIPathString() { - return getLocalFileAsURIPathString(Paths.get(".")); - } - - /** - * Get just the path part of the URI representing the current working directory. Used - * to construct expected URI string values for test inputs that specify a file in the - * root of the local file system. - * - * Returns a string of the form "/" or "/d:/" on Windows. - */ - private String getRootDirectoryAsURIPathString() { - return getLocalFileAsURIPathString(Paths.get(FS_SEPARATOR)); - } - - /** - * Get just the path part of the URI representing a file on the local file system as a normalized - * String. - * - * Returns a string of the form `/some/path' or '/d:/some/path/` on Windows. - */ - private String getLocalFileAsURIPathString(final Path localPath) { - return localPath.toUri().normalize().getPath(); - } - -} diff --git a/core/src/test/scala/org/htsjdk/core/UnitTest.scala b/core/src/test/scala/org/htsjdk/core/UnitTest.scala new file mode 100644 index 0000000..13045de --- /dev/null +++ b/core/src/test/scala/org/htsjdk/core/UnitTest.scala @@ -0,0 +1,33 @@ +package org.htsjdk.core + +import java.nio.file.{Files, Path, Paths} + +import org.scalatest.{FlatSpec, Matchers} + +/** Base class for all Scala tests. */ +class UnitTest extends FlatSpec with Matchers { + /** Make a temporary file that will get cleaned up at the end of testing. */ + protected def makeTempFile(prefix: String, suffix: String): Path = { + val path = Files.createTempFile(prefix, suffix) + path.toFile.deleteOnExit() + path + } + + /** Implicit conversion from Java to Scala iterator. */ + implicit def javaIteratorAsScalaIterator[A](iter: java.util.Iterator[A]): Iterator[A] = { + scala.collection.JavaConverters.asScalaIterator(iter) + } + + /** Implicit conversion from Java to Scala iterable. */ + implicit def javaIterableAsScalaIterable[A](iterable: java.lang.Iterable[A]): Iterable[A] = { + scala.collection.JavaConverters.iterableAsScalaIterable(iterable) + } + + /** Small implicit class to give operator like syntax to paths. */ + implicit class PathLike(path: Path) { + def /(sub: String): Path = path.resolve(sub) + } + + /** Generates a PathLike from a String instead of a Path. */ + implicit def stringToPathLike(path: String): PathLike = PathLike(Paths.get(path)) +} diff --git a/core/src/test/scala/org/htsjdk/core/utils/PathSpecifierTest.scala b/core/src/test/scala/org/htsjdk/core/utils/PathSpecifierTest.scala new file mode 100644 index 0000000..5e62c5a --- /dev/null +++ b/core/src/test/scala/org/htsjdk/core/utils/PathSpecifierTest.scala @@ -0,0 +1,197 @@ +package org.htsjdk.core.utils + +import java.io.{DataInputStream, DataOutputStream} +import java.nio.file.{FileSystems, Path, Paths} + +import com.google.common.jimfs.{Configuration, Jimfs} +import org.apache.commons.lang3.SystemUtils +import org.htsjdk.core.UnitTest + +class PathSpecifierTest extends UnitTest { + private val Sep = FileSystems.getDefault.getSeparator + private val CwdPath: String = Paths.get(".").normalize().toAbsolutePath.toString + private val CwdUriPath: String = Paths.get(".").toUri.normalize().getPath + private val RootUriPath: String = Paths.get("/").toUri.normalize().getPath + private val StdIn: String = if (SystemUtils.IS_OS_WINDOWS) "-" else "/dev/stdin" + private val StdOut: String = if (SystemUtils.IS_OS_WINDOWS) "-" else "/dev/stdout" + + case class Test(input: String, uri: String, nio: Boolean, path: Boolean) + object Test { + def apply(input: Path, uri: String, nio: Boolean, path: Boolean): Test = Test(input.toString, uri, nio, path) + } + + val ValidInputs = Seq( + Test("localFile.bam", "file://" + CwdUriPath + "localFile.bam", nio = true, path = true), + // absolute reference to a file in the root of the current file system (Windows accepts the "/" as root) + Test("/localFile.bam", "file://" + RootUriPath + "localFile.bam", nio = true, path = true), + // absolute reference to a file in the root of the current file system, where root is specified using the default FS separator + Test(Sep + "localFile.bam", "file://" + RootUriPath + "localFile.bam", nio = true, path = true), + // absolute reference to a file + Test("/path" / "to" / "localFile.bam", "file://" + RootUriPath + "path/to/localFile.bam", nio = true, path = true), + // absolute reference to a file that contains a URI "excluded" character in the path ("#"), which without + // encoding will be treated as a fragment delimiter + Test("/project" / "gvcf-pcr" / "23232_1#1" / "1.g.vcf.gz", "file://" + RootUriPath+ "project/gvcf-pcr/23232_1%231/1.g.vcf.gz", nio = true, path = true), + // relative reference to a file on the local file system + Test("path" / "to" / "localFile.bam", "file://" + CwdUriPath + "path/to/localFile.bam", nio = true, path = true), + // Windows also accepts "/" as a valid root specifier + Test("/", "file://" + RootUriPath, nio = true, path = true), + Test(".", "file://" + CwdUriPath + "./", nio = true, path = true), + Test("../.", "file://" + CwdUriPath + ".././", nio = true, path = true), + // an empty path is equivalent to accessing the current directory of the default file system + Test("", "file://" + CwdUriPath, nio = true, path = true), + + //*********************************************************** + // Local file references using a URI with a "file://" scheme. + //*********************************************************** + + Test("file:localFile.bam", "file:localFile.bam", nio = true, path = false), // absolute, opaque (not hierarchical) + Test("file:/localFile.bam", "file:/localFile.bam", nio = true, path = true), // absolute, hierarchical + Test("file://localFile.bam", "file://localFile.bam", nio = true, path = false), // file URLs can't have an authority ("localFile.bam") + Test("file:///localFile.bam", "file:///localFile.bam", nio = true, path = true), // empty authority + Test("file:path/to/localFile.bam", "file:path/to/localFile.bam", nio = true, path = false), + Test("file:/path/to/localFile.bam", "file:/path/to/localFile.bam", nio = true, path = true), + // "path" appears to be an authority, and will be accepted on Windows since this URI will be + // interpreted as a UNC path containing an authority + Test("file://path/to/localFile.bam", "file://path/to/localFile.bam", nio = true, SystemUtils.IS_OS_WINDOWS), + // "localhost" is accepted as a special case authority for "file://" Paths on Windows; but not Linux/Mac + Test("file://localhost/to/localFile.bam","file://localhost/to/localFile.bam", nio = true, SystemUtils.IS_OS_WINDOWS), + Test("file:///path/to/localFile.bam", "file:///path/to/localFile.bam", nio = true, path = true), // empty authority + + //***************************************************************************** + // Valid URIs which are NOT valid NIO paths (no installed file system provider) + //***************************************************************************** + + Test("gs://file.bam", "gs://file.bam", nio = false, path = false), + Test("gs://bucket/file.bam", "gs://bucket/file.bam", nio = false, path = false), + Test("gs:///bucket/file.bam", "gs:///bucket/file.bam", nio = false, path = false), + Test("gs://auth/bucket/file.bam", "gs://auth/bucket/file.bam", nio = false, path = false), + Test("gs://hellbender/test/resources/", "gs://hellbender/test/resources/", nio = false, path = false), + Test("gcs://abucket/bucket", "gcs://abucket/bucket", nio = false, path = false), + Test("gendb://somegdb", "gendb://somegdb", nio = false, path = false), + Test("chr1:1-100", "chr1:1-100", nio = false, path = false), + + //***************************************************************************************** + // Valid URIs which are backed by an installed NIO file system provider), but are which not + // actually resolvable as paths because the scheme-specific part is not valid for one reason + // or another. + //********************************************************************************************** + + // uri must have a path: jimfs:file.bam + Test("jimfs:file.bam", "jimfs:file.bam", nio = true, path = false), + // java.lang.AssertionError: java.net.URISyntaxException: Expected scheme-specific part at index 6: jimfs: + Test("jimfs:/file.bam", "jimfs:/file.bam", nio = true, path = false), + // java.lang.AssertionError: uri must have a path: jimfs://file.bam + Test("jimfs://file.bam", "jimfs://file.bam", nio = true, path = false), + // java.lang.AssertionError: java.net.URISyntaxException: Expected scheme-specific part at index 6: jimfs: + Test("jimfs:///file.bam", "jimfs:///file.bam", nio = true, path = false), + // java.nio.file.FileSystemNotFoundException: jimfs://root + Test("jimfs://root/file.bam","jimfs://root/file.bam", nio = true, path = false), + + //*********************************************************************************************** + // References that contain characters that require URI-encoding. If the input string is presented + // without no scheme, it will be be automatically encoded by PathSpecifier, otherwise it + // must already be URI-encoded. + //*********************************************************************************************** + + // relative (non-URI) reference to a file on the local file system that contains a URI fragment delimiter + // is automatically URI-encoded + Test("project" / "gvcf-pcr" / "23232_1#1" / "1.g.vcf.gz", "file://" + CwdUriPath + "project/gvcf-pcr/23232_1%231/1.g.vcf.gz", nio = true, path = true), + // URI reference with fragment delimiter is not automatically URI-encoded + Test("file:project/gvcf-pcr/23232_1#1/1.g.vcf.gz", "file:project/gvcf-pcr/23232_1#1/1.g.vcf.gz", nio = true, path = false), + Test("file:/project/gvcf-pcr/23232_1#1/1.g.vcf.gz", "file:/project/gvcf-pcr/23232_1#1/1.g.vcf.gz", nio = true, path = false), + Test("file:///project/gvcf-pcr/23232_1%231/1.g.vcf.g", "file:///project/gvcf-pcr/23232_1%231/1.g.vcf.g", nio = true, path = true), + ) + + "PathSpecifier" should "be able to open an input stream from stdin" in { + val spec = new PathSpecifier(StdIn) + val in = new DataInputStream(spec.getInputStream) + val bytes = new Array[Byte](0) + in.readFully(bytes) + new String(bytes) shouldBe "" + } + + it should "be able to open up an output stream from stdout" in { + val spec = new PathSpecifier(StdOut) + val out = new DataOutputStream(spec.getOutputStream) + out.write("some stuff".getBytes) + } + + ValidInputs.zipWithIndex.foreach { case(in, index) => + it should s"correctly handle input #$index '${in.input}'" in { + val spec = new PathSpecifier(in.input) + spec.getURIString shouldBe in.uri + spec.isNIO shouldBe in.nio + spec.isPath shouldBe in.path + if (in.path) spec.toPath should not be null + } + } + + it should "throw an exception on an invalid input string" in { + an[IllegalArgumentException] shouldBe thrownBy { new PathSpecifier("\u0000")} + } + + Seq( + ("file:/project/gvcf-pcr/23232_1#1/1.g.vcf.gz", "not URL encoded"), + ("file:project/gvcf-pcr/23232_1#1/1.g.vcf.gz", "scheme-specific part is not hierarchical"), + ("hdfs://userinfo@host:80/path/to/file.bam", "hdfs doesn't allow invalid hosts"), + ("unknownscheme://foobar", "URI with an unknown scheme"), + ("gendb://adb", "???"), + ("gcs://abucket/bucket", "valid URI that isn't a path"), + ("file://nonexistent_authority/path/to/file.bam", "file URI with unknown authority nonexistent_authority") + ).foreach { case (input, desc) => + it should s"not see $input as a path because: $desc" in { + val spec = new PathSpecifier(input) + spec.isPath shouldBe false + an[Exception] shouldBe thrownBy { spec.toPath } + } + } + + it should "work with a non-default NIO filesystem" in { + val jimfs = Jimfs.newFileSystem(Configuration.unix) + val outputPath = jimfs.getPath("alternateFileSystemTest.txt") + val contents = "Test contents" + val spec = new PathSpecifier(outputPath.toUri.toString) + val out = new DataOutputStream(spec.getOutputStream) + out.write(contents.getBytes) + out.close() + + // read it back in and make sure it matches expected contents + val in = new DataInputStream(spec.getInputStream) + val bytes = new Array[Byte](contents.length * 2) + val count = in.read(bytes) + in.close() + jimfs.close() + count shouldBe contents.length + new String(bytes, 0, count) shouldBe contents + } + + Seq( + (".." / "data" / "utils" / "testTextFile.txt", "Test file."), // relative (file) reference to a local file + (CwdPath / ".." / "data" / "utils" / "testTextFile.txt", "Test file."), // absolute reference to a local file + (s"file://$CwdUriPath//../data/utils/testTextFile.txt", "Test file."), // URI reference to a local file, where the path is absolute + + // reference to a local file with an embedded fragment delimiter ("#") in the name; if the file + // scheme is included, the rest of the path must already be encoded; if no file scheme is + // included, the path is encoded by the PathSpecifier class + (".." / "data" / "utils" / "testDirWith#InName" / "testTextFile.txt", "Test file."), + (s"file://$CwdUriPath/../data/utils/testDirWith%23InName/testTextFile.txt", "Test file."), + ).foreach { case (input, contents) => + it should s"be able to generate an input stream from $input" in { + val spec = new PathSpecifier(input.toString) + val stream = new DataInputStream(spec.getInputStream) + val bytes = new Array[Byte](contents.length * 2) + val count = stream.read(bytes) + stream.close() + + count shouldBe contents.length + new String(bytes, 0, count) shouldBe contents + } + } + + it should "be able to generate an output stream for valid paths" in { + val path = makeTempFile("some_text_file.", ".txt") + val pathOnlyString = path.toUri().normalize().getPath() + new PathSpecifier(path.toString()).getOutputStream.close() + new PathSpecifier(s"file://$pathOnlyString" ).getOutputStream.close() + } +} diff --git a/data/simple.bam b/data/simple.bam new file mode 100644 index 0000000..6b935be Binary files /dev/null and b/data/simple.bam differ diff --git a/data/utils/testDirWith#InName/testTextFile.txt b/data/utils/testDirWith#InName/testTextFile.txt index f5d299f..7b03026 100644 --- a/data/utils/testDirWith#InName/testTextFile.txt +++ b/data/utils/testDirWith#InName/testTextFile.txt @@ -1,2 +1 @@ -Test file. - +Test file. \ No newline at end of file diff --git a/data/utils/testTextFile.txt b/data/utils/testTextFile.txt index f5d299f..7b03026 100644 --- a/data/utils/testTextFile.txt +++ b/data/utils/testTextFile.txt @@ -1,2 +1 @@ -Test file. - +Test file. \ No newline at end of file