diff --git a/docker/install-dev-layer.sh b/docker/install-dev-layer.sh index 2b2d57b9..11d0b710 100755 --- a/docker/install-dev-layer.sh +++ b/docker/install-dev-layer.sh @@ -15,8 +15,8 @@ # Must have $INSTALL_PATH and $VIRAL_NGS_PATH defined (from viral-core docker) set -e -o pipefail -cd /opt/viral-ngs/viral-assemble +VIRAL_ASSEMBLE_PATH=/opt/viral-ngs/viral-assemble -$VIRAL_NGS_PATH/docker/install-conda-dependencies.sh requirements-conda.txt +$VIRAL_NGS_PATH/docker/install-conda-dependencies.sh $VIRAL_ASSEMBLE_PATH/requirements-conda.txt -ln -s assemble test assembly.py $VIRAL_NGS_PATH +ln -s $VIRAL_ASSEMBLE_PATH/assemble $VIRAL_ASSEMBLE_PATH/test $VIRAL_ASSEMBLE_PATH/assembly.py $VIRAL_NGS_PATH diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 00000000..75b09b2f --- /dev/null +++ b/test/__init__.py @@ -0,0 +1,122 @@ +'''utilities for tests''' + +__author__ = "irwin@broadinstitute.org" + +# built-ins +import filecmp +import os +import re +import unittest +import hashlib +import logging + +# third-party +import Bio.SeqIO +import pytest + +# intra-project +import util.file +from util.misc import available_cpu_count +from tools.samtools import SamtoolsTool + +logging.getLogger('botocore').setLevel(logging.WARNING) +logging.getLogger('boto3').setLevel(logging.WARNING) + + +if 'PYTEST_XDIST_WORKER_COUNT' in os.environ: + _CPUS = 1 +else: + _CPUS = available_cpu_count() + + +def assert_equal_contents(testCase, filename1, filename2): + 'Assert contents of two files are equal for a unittest.TestCase' + testCase.assertTrue(filecmp.cmp(filename1, filename2, shallow=False)) + + +def assert_equal_bam_reads(testCase, bam_filename1, bam_filename2): + ''' Assert that two bam files are equivalent + + This function converts each file to sam (plaintext) format, + without header, since the header can be variable. + + Test data should be stored in bam format rather than sam + to save space, and to ensure the bam->sam conversion + is the same for both files. + ''' + + samtools = SamtoolsTool() + + sam_one = util.file.mkstempfname(".sam") + sam_two = util.file.mkstempfname(".sam") + + # write the bam files to sam format, without header (no -h) + samtools.view(args=[], inFile=bam_filename1, outFile=sam_one) + samtools.view(args=[], inFile=bam_filename2, outFile=sam_two) + + try: + testCase.assertTrue(filecmp.cmp(sam_one, sam_two, shallow=False)) + finally: + for fname in [sam_one, sam_two]: + if os.path.exists(fname): + os.unlink(fname) + +def assert_md5_equal_to_line_in_file(testCase, filename, checksum_file, msg=None): + ''' Compare the checksum of a test file with the expected checksum + stored in a second file + compare md5(test_output.bam) with expected_output.bam.md5:1 + ''' + + hash_md5 = hashlib.md5() + with open(filename, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_md5.update(chunk) + + expected_checksum = "" + with open(checksum_file, "rb") as f: + expected_checksum = str(f.readline().decode("utf-8")) + + expected_checksum = expected_checksum.replace("\r","").replace("\n","") + + assert len(expected_checksum) > 0 + + testCase.assertEqual(hash_md5.hexdigest(), expected_checksum, msg=msg) + +@pytest.mark.usefixtures('tmpdir_class') +class TestCaseWithTmp(unittest.TestCase): + 'Base class for tests that use tempDir' + + def assertEqualContents(self, f1, f2): + assert_equal_contents(self, f1, f2) + + def assertEqualFasta(self, f1, f2): + '''Check that two fasta files have the same sequence ids and bases''' + def seqIdPairs(f): + return [(rec.id, rec.seq) for rec in Bio.SeqIO.parse(f, 'fasta')] + self.assertEqual(seqIdPairs(f1), seqIdPairs(f2)) + + def assertEqualFastaSeqs(self, f1, f2): + '''Check that two fasta files have the same sequence bases (sequence ids may differ)''' + def seqs(f): + return [rec.seq for rec in Bio.SeqIO.parse(f, 'fasta')] + self.assertEqual(seqs(f1), seqs(f2)) + + def input(self, fname): + '''Return the full filename for a file in the test input directory for this test class''' + return os.path.join(util.file.get_test_input_path(self), fname) + + def inputs(self, *fnames): + '''Return the full filenames for files in the test input directory for this test class''' + return [self.input(fname) for fname in fnames] + +""" +When "nose" executes python scripts for automated testing, it excludes ones with +the executable bit set (in case they aren't import safe). To prevent any of the +tests in this folder from being silently excluded, assure this bit is not set. +""" + + +def assert_none_executable(): + testDir = os.path.dirname(__file__) + assert all(not os.access(os.path.join(testDir, filename), os.X_OK) for filename in os.listdir(testDir) + if filename.endswith('.py')) diff --git a/test/stubs.py b/test/stubs.py new file mode 100644 index 00000000..dffbabb0 --- /dev/null +++ b/test/stubs.py @@ -0,0 +1,79 @@ +import logging +import os +import pytest +import tools + + +class StubCondaPackage(tools.Tool): + + # Skip gathering in all_tool_classes + _skiptest = True + + def __init__( + self, + package, + channel="bioconda", + executable=None, + version="", + verifycmd=None, + verifycode=0, + require_executability=True, + env=None, + env_root_path=None, + conda_cache_path=None, + patches=None, + post_install_command=None, + post_install_ret=0, + post_verify_command=None, + post_verify_ret=0 + ): + self.executable = executable or package + if type(version) == tools.CondaPackageVersion: + self.version = version + else: + self.version = tools.CondaPackageVersion(version) + + self.env_path = None + if 'CONDA_PREFIX' in os.environ and len(os.environ['CONDA_PREFIX']): + last_path_component = os.path.basename(os.path.normpath(os.environ['CONDA_PREFIX'])) + self.env_path = os.path.dirname(os.environ['CONDA_PREFIX']) if last_path_component == "bin" else os.environ['CONDA_PREFIX'] + + else: + raise Exception + + def is_attempted(self): + return True + + def is_installed(self): + return True + + @property + def bin_path(self): + return os.path.join(self.env_path, 'bin') + + def executable_path(self): + return os.path.join(self.bin_path, self.executable) + + def apply_patches(self): + pass + + def verify_install(self): + return + + def package_available(self): + return True + + def uninstall_package(self): + return True + + def install_package(self): + return True + + def get_installed_version(self): + return self.version + + def post_install(self): + pass + + def execute(self, cmd, loglevel=logging.DEBUG): + return {}