# Copyright (c) 2013-2020, SIB - Swiss Institute of Bioinformatics and
#                          Biozentrum - University of Basel
# 
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# 
#   http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""
Testing our own little argument parser.
"""

import unittest
import tempfile
import gzip
import json
import ost
from promod3.core import pm3argparse


class _FetchLog(ost.LogSink):
    """
    Capturing output of the OST logger
    """
    def __init__(self):
        ost.LogSink.__init__(self)
        self.messages = dict()

    def LogMessage(self, message, severity):
        levels = ['ERROR', 'WARNING', 'SCRIPT', 'INFO', 'VERBOSE', 'DEBUG',
                  'TRACE']
        level = levels[severity]
        if not level in list(self.messages.keys()):
            self.messages[level] = list()
        self.messages[level].append(message.strip())

def _GetJsonTestObj():
    '''Create a JSOn object for testing
    '''
    obj = dict()
    ali = dict()
    ali['target'] = {'name': 'TARGET',
                     'seqres':
                     'VLSPADKTNVKAAWGKVGAHAGEYGAEALERMFLSFPTTKTYFPHF-DLS-----'+
                     'HGSAQVKGHGKKVADALTNAVAHVDDMPNALSALSDLHAHK-LRVDPVNFKLLSH'+
                     'CLLVTLAAHLPAEFTPAVHASLDKFLASVSTVLTSKYR'}
    ali['template'] = {'name': 'TEMPLATE',
                       'seqres':
                       'HLTPEEKSAVTALWGKVN--VDEVGGEALGRLLVVYPWTQRFFESFGDLSTPD'+
                       'AVMGNPKVKAHGKKVLGAFSDGLAHLDNLKGTFATLSELHC-DKLHVDPENFR'+
                       'LLGNVLVCVLAHHFGKEFTPPVQAAYQKVVAGVANALAHKYH',
                       'offset': 0}
    obj['alignmentlist'] = list()
    obj['alignmentlist'].append(ali)

    return obj

class PM3ArgParseTests(unittest.TestCase):
    def setUp(self):
        self.log = _FetchLog()
        ost.PushLogSink(self.log)
        ost.PushVerbosityLevel(2)

    def tearDown(self):
        ost.PopVerbosityLevel()
        ost.PopLogSink()

    def testUnrecognisedArguments(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['-x'])
        self.assertEqual(ecd.exception.code, 2)
        self.assertEqual(len(self.log.messages['ERROR']), 2)
        self.assertEqual(self.log.messages['ERROR'],
                         ['usage: test_pm3argparse.py [-h]',
                          'test_pm3argparse.py: error: unrecognized '+
                          'arguments: -x'])

    def testActionSwitch(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--help'])
        self.assertEqual(ecd.exception.code, 0)
        self.assertEqual(self.log.messages['SCRIPT'],
                         ['usage: test_pm3argparse.py [-h]\n\nTesting our '+
                          'own little argument parser.\n\noptional '+
                          'arguments:\n  -h, --help  show this help message '+
                          'and exit'])

        self.log.messages = dict()
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=True)
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--help'])
        self.assertEqual(ecd.exception.code, 0)
        self.assertEqual(self.log.messages['SCRIPT'],
                         ['usage: t_pm3argparse.py [-h]\n\nTesting our own '+
                          'little argument parser.\n\noptional '+
                          'arguments:\n  -h, --help  show this help message '+
                          'and exit'])


    def testDescription(self):
        parser = pm3argparse.PM3ArgumentParser(action=False,
                                               description='This is a test.')
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--help'])
        self.assertEqual(ecd.exception.code, 0)
        self.assertEqual(self.log.messages['SCRIPT'],
                         ['usage: test_pm3argparse.py [-h]\n\nThis is a '+
                          'test.\n\noptional arguments:\n  -h, --help  show '+
                          'this help message and exit'])

    def testAddAlignmentNoFileArg(self):
        # check failure on missing file argument
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--fasta'])
        self.assertEqual(ecd.exception.code, 2)
        self.assertEqual(len(self.log.messages['ERROR']), 2)
        self.assertEqual(self.log.messages['ERROR'],
                         ['usage: test_pm3argparse.py [-h] (-f '+
                          '<FILE> | -c <FILE> | -j <OBJECT>|<FILE>)',
                          'test_pm3argparse.py: error: argument -f/--fasta: '+
                          'expected one argument'])

    def testAddAlignmentNoFile(self):
        # check that we throw an error if a non-exisiting file is given
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--fasta', 'notexistingfile.fas'])
        self.assertEqual(ecd.exception.code, 12)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'][0],
                         "Alignment file does not exist: notexistingfile.fas")

    def testAddAlignmentEmpty(self):
        # we want to fail if we get an empty FastA file
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--fasta', 'data/fasta/alignment.fas'])
        self.assertEqual(ecd.exception.code, 15)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'][0],
                         "'--fasta data/fasta/alignment.fas' refers "+
                         "to an empty file or its in the wrong format.")

    def testAddAlignmentSingleSequence(self):
        # we want to fail if we get a fasta with 1 seq.
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--fasta', 'data/fasta/1ake_1.fas'])
        self.assertEqual(ecd.exception.code, 16)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'][0],
                         "'--fasta data/fasta/1ake_1.fas' points to an "+
                         "alignment with only 1 sequence.")

    def testAddAlignmentToManyDisallowed(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment(allow_multitemplate=False)
        parser.AssembleParser()
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--fasta', 'data/fasta/1ake_3.fas'])
        self.assertEqual(ecd.exception.code, 16)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'][0],
                         "'--fasta data/fasta/1ake_3.fas' points to an "+
                         "alignment with more than 2 sequences and we do "+
                         "not allow this.")

    def testAddAlignmentMultipleTargets(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment(allow_multitemplate=True)
        parser.AssembleParser()
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--fasta', 'data/fasta/1ake_2trg.fas'])
        self.assertEqual(ecd.exception.code, 17)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'][0], "'--fasta "+
                         "data/fasta/1ake_2trg.fas': multiple targets found!")

    def testAddAlignmentDifferentSeqLens(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--fasta', 'data/fasta/1ake_nel.fas'])
        self.assertEqual(ecd.exception.code, 18)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'][0], "'--fasta "+
                         "data/fasta/1ake_nel.fas': error when reading "+
                         "alignment file: sequences have different lengths")

    def testAddAlignmentGzipIn(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        opts = parser.Parse(['--fasta', 'data/fasta/1ake.fas.gz',
                             '--fasta', 'data/fasta/1ake.fas'])
        self.assertEqual(str(opts.alignments[0]), 'target  APGAGKGTQAQFIMEKYG'+
                         'IPQISTGGGLRAAVKS---LGKQAKDIMDAGKLVTDELVIALVKERIAQED'+
                         'CRN\n1AKE.B  APGAGKGTQAQFIMEKYGIPQISTGDMLRAAVKSGSEL'+
                         'GKQAKDIMDAGKLVTDELVIALVKERIAQEDCRN\n\ntarget  GFLLD'+
                         'GFPRTIPQADAMKEAGINVDYVLEF----ELIVDRIVGRRVHAPSGRVYHV'+
                         'KFNPPKVEGKDDVTGE\n1AKE.B  GFLLDGFPRTIPQADAMKEAGINVD'+
                         'YVLEFDVPDELIVDRIVGRRVHAPSGRVYHVKFNPPKVEGKDDVTGE\n\n'+
                         'target  ELTTRKDDQEETVRKRLVEYHQMTAPLL--YYYYKEAEAGNTK'+
                         'YAKVDGTKPVAEVRADLEKILG\n1AKE.B  ELTTRKDDQEETVRKRLVE'+
                         'YHQMTAPLIGYYYYSKEAEAGNTKYAKVDGTKPV---AEVRADLEK\n')
        self.assertEqual(str(opts.alignments[0]), str(opts.alignments[1]))

    def testAddAlignmentGzipNoExist(self):
        # using a gzip FastA file which does not exist
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--fasta', 'data/fasta/notthere.fas.gz'])
        self.assertEqual(ecd.exception.code, 12)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'][0],
                         "Alignment file does not exist: "+
                         "data/fasta/notthere.fas.gz")

    def testAddAlignmentSwitchSeqs(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        opts = parser.Parse(['--fasta', 'data/fasta/1ake.fas'])
        self.assertEqual(str(opts.alignments[0].GetSequence(0)),
                         'APGAGKGTQAQFIMEKYGIPQISTGGGLRAAVKS---LGKQAKDIMDAGK'+
                         'LVTDELVIALVKERIAQEDCRNGFLLDGFPRTIPQADAMKEAGINVDYVL'+
                         'EF----ELIVDRIVGRRVHAPSGRVYHVKFNPPKVEGKDDVTGEELTTRK'+
                         'DDQEETVRKRLVEYHQMTAPLL--YYYYKEAEAGNTKYAKVDGTKPVAEV'+
                         'RADLEKILG')

        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        opts = parser.Parse(['--fasta', 'data/fasta/1ake_sw.fas'])
        self.assertEqual(str(opts.alignments[0].GetSequence(0)),
                         'APGAGKGTQAQFIMEKYGIPQISTGGGLRAAVKS---LGKQAKDIMDAGK'+
                         'LVTDELVIALVKERIAQEDCRNGFLLDGFPRTIPQADAMKEAGINVDYVL'+
                         'EF----ELIVDRIVGRRVHAPSGRVYHVKFNPPKVEGKDDVTGEELTTRK'+
                         'DDQEETVRKRLVEYHQMTAPLL--YYYYKEAEAGNTKYAKVDGTKPVAEV'+
                         'RADLEKILG')

    def testAddAlignment(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        opts = parser.Parse(['--fasta', 'data/fasta/1ake.fas'])
        self.assertEqual(len(opts.alignments), 1)
        self.assertEqual(opts.alignments[0].GetLength(), 209)
        self.assertEqual(opts.alignments[0].GetSequenceOffset(0), 0)
        self.assertEqual(opts.alignments[0].GetSequenceOffset(1), 0)
        self.assertEqual(opts.alignments[0].GetSequence(0).gapless_string,
                         'APGAGKGTQAQFIMEKYGIPQISTGGGLRAAVKSLGKQAKDIMDAGKLVT'+
                         'DELVIALVKERIAQEDCRNGFLLDGFPRTIPQADAMKEAGINVDYVLEFE'+
                         'LIVDRIVGRRVHAPSGRVYHVKFNPPKVEGKDDVTGEELTTRKDDQEETV'+
                         'RKRLVEYHQMTAPLLYYYYKEAEAGNTKYAKVDGTKPVAEVRADLEKILG')
        self.assertEqual(opts.alignments[0].GetSequence(0).name, 'target')
        self.assertEqual(opts.alignments[0].GetSequence(1).name, '1AKE.B')
        self.assertEqual(opts.aln_sources[0], 'data/fasta/1ake.fas')

    def testAddAlignmentToMany(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment(allow_multitemplate=True)
        parser.AssembleParser()
        opts = parser.Parse(['--fasta', 'data/fasta/1ake_3.fas'])
        self.assertEqual(len(opts.alignments), 1)
        self.assertEqual(opts.alignments[0].GetCount(), 3)

    def testAddAlignmentNoneGiven(self):
        # if we have none of '--fasta', '--json', fail.
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse([])
        self.assertEqual(ecd.exception.code, 2)
        self.assertEqual(len(self.log.messages['ERROR']), 2)
        self.assertEqual(len(self.log.messages['ERROR']), 2)
        self.assertEqual(self.log.messages['ERROR'],
                         ['usage: test_pm3argparse.py [-h] (-f '+
                          '<FILE> | -c <FILE> | -j <OBJECT>|<FILE>)',
                          'test_pm3argparse.py: error: one of the arguments '+
                          '-f/--fasta -c/--clustal -j/--json is required'])

    def testAddAlignmentFastaAndJson(self):
        # testing that --fasta and --json DO NOT work together
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--fasta', 'data/fasta/1ake.fas',
                          '--json', 'foo'])
        self.assertEqual(ecd.exception.code, 2)
        self.assertEqual(len(self.log.messages['ERROR']), 2)
        self.assertEqual(self.log.messages['ERROR'],
                         ['usage: test_pm3argparse.py [-h] (-f '+
                          '<FILE> | -c <FILE> | -j <OBJECT>|<FILE>)',
                          'test_pm3argparse.py: error: argument -j/--json: '+
                          'not allowed with argument -f/--fasta'])

    def testAddAlignmentJsonNoArg(self):
        # make sure --json always needs an argument
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--json'])
        self.assertEqual(ecd.exception.code, 2)
        self.assertEqual(len(self.log.messages['ERROR']), 2)
        self.assertEqual(self.log.messages['ERROR'],
                         ['usage: test_pm3argparse.py [-h] (-f '+
                          '<FILE> | -c <FILE> | -j <OBJECT>|<FILE>)',
                          'test_pm3argparse.py: error: argument -j/--json: '+
                          'expected one argument'])

    def testAddAlignmentJsonFileNoExist(self):
        # fail when a non-existing file is handed over to --json
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--json', 'fileshouldnotexist'])
        self.assertEqual(ecd.exception.code, 12)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'],
                         ["JSON Alignment file does not exist: " +
                          "fileshouldnotexist"])

    def testAddAlignmentJsonGzipNoExist(self):
        # lets see what happens on json with a missing gzip file
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--json', 'fileshouldnotexist.gzip'])
        self.assertEqual(ecd.exception.code, 12)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'],
                         ["JSON Alignment file does not exist: " +
                          "fileshouldnotexist.gzip"])

    def testAddAlignmentJsonEmptyFile(self):
        # we want to fail on empty JSON files
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        tmp_ali = tempfile.NamedTemporaryFile(suffix='.json')
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--json', tmp_ali.name])
        tmp_ali.close()
        self.assertEqual(ecd.exception.code, 20)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'],
                         ["'--json' file '%s' could not be " % tmp_ali.name+
                          "processed into a JSON object, probably it's empty."])

    def testAddAlignmentJsonNoAlnLstLstKey(self):
        # check that 'alignmentlist' features a list
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        json_str = json.dumps({'alignmentlist': "I'm not a list!"})
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--json', json_str])
        self.assertEqual(ecd.exception.code, 24)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'],
                         ['JSON object from \'' + json_str + 
                          '\' does notprovide a list behind \'alignmentlist\'.'])

    def testAddAlignmentJsonMalString(self):
        # fail on improper JSON string
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        json_str = "{'Wrong': 'wrong'}"
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--json', json_str])
        self.assertEqual(ecd.exception.code, 23)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertTrue(self.log.messages['ERROR'][0].startswith(
          "'--json' string '{'Wrong': 'wrong'}' could not be decoded:"))

    def testAddAlignmentJsonNoAlnLstKey(self):
        # detect missing key 'alignmentlist
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        json_str = json.dumps({"Sth different": "Foo"})
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--json', json_str])
        self.assertEqual(ecd.exception.code, 21)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'],
                         ['JSON object from \'' + json_str + 
                          '\' does not provide an \'alignmentlist\' key.'])

    def testAddAlignmentJsonNoTargetKey(self):
        # check that 'alignmentlist'target' is required
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        json_str = json.dumps({'alignmentlist': [{'Foo': 'BAR'}]})
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--json', json_str])
        self.assertEqual(ecd.exception.code, 22)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'],
                         ['JSON \'alignmentlist\' from \'' + json_str + 
                          '\' does not provide a \'target\' key.'])

    def testAddAlignmentJsonNoTemplateKey(self):
        # check that 'alignmentlist'template' is required
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        json_str = '{\"alignmentlist\": [{\"target\": {\"seqres\": \"AA\", '\
                   '\"name\": \"AAA\"}}]}'
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--json', json_str])
        self.assertEqual(ecd.exception.code, 22)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'],
                         ['JSON \'alignmentlist\' from \'' + json_str + 
                          '\' does not provide a \'template\' key.'])

    def testAddAlignmentJsonAlnTrgNoDict(self):
        # entries of the alignmentlist need to be dict's
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        json_str = json.dumps({'alignmentlist': ['Seq1', 'Seq2']})
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--json', json_str])
        self.assertEqual(ecd.exception.code, 25)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'],
                         ['JSON \'alignmentlist\' member from \'' + json_str +
                          '\' is not a \'  dictionary: Seq1'])

    def testAddAlignmentJsonAlnTrgNotDict(self):
        # entries of the alignmentlist need to be dict's of dict's
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        json_str = json.dumps({'alignmentlist': [{'target': 'AAA',
                                                  'template': 'BBB'}]})
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--json', json_str])
        self.assertEqual(ecd.exception.code, 26)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'],
                         ['JSON \'alignmentlist\' \'target\' '+
                          'from\'' + json_str + '\' is not a dictionary: AAA'])

    def testAddAlignmentJsonAlnTrgNoNameNoSeqres(self):
        # entries of the alignmentlist need to be dict's
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        json_str = json.dumps({'alignmentlist': [{'target': {'AAA': 1},
                                                  'template': {'BBB': 2}}]})
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--json', json_str])
        self.assertEqual(ecd.exception.code, 27)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'],
                         ['JSON \'alignmentlist\' \'target\' from '+
                          '\'' + json_str + '\' is missing the \'name\' key'])

    def testAddAlignmentJsonAlnTrgTplNoString(self):
        # entries of the sequence dict in an aln need to be str
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        json_str = '{"alignmentlist": [{"target": {"name": 1, "seqres": 2}, '+\
                   '"template": {"name": 2, "seqres": 2}}]}'
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--json', json_str])
        self.assertEqual(ecd.exception.code, 28)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'],
                         ['JSON \'alignmentlist\' \'target\' \'name\' '+
                          'from\'' + json_str + '\' is not a <class \'str\'>'])

    def testAddAlignmentJsonAlnTplNoOffset(self):
        # no offset for template sequence
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        json_str = json.dumps({'alignmentlist': [{'target': {'name': 'A',
                                                             'seqres': 'AA'},
                                                  'template': {'name': 'A',
                                                               'seqres': 'AA'}}
                                                ]})
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--json', json_str])
        self.assertEqual(ecd.exception.code, 27)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'],
                         ['JSON \'alignmentlist\' \'template\' from '+
                          '\'' + json_str + '\' is missing the \'offset\' key'])

    def testAddAlignmentJsonAlnTplOffsetStr(self):
        # entries of the alignmentlist need to be dict's
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        json_str = json.dumps({'alignmentlist': [{'target': {'name': 'A',
                                                             'seqres': 'AA'},
                                                  'template': {'name': 'A',
                                                               'seqres': 'AA',
                                                               'offset': '0'}}
                                                ]})
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--json', json_str])
        self.assertEqual(ecd.exception.code, 28)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'],
                         ['JSON \'alignmentlist\' \'template\' \'offset\' '+
                          'from\'' + json_str + '\' is not a <class \'int\'>'])

    def testAddAlignmentJsonAlnString(self):
        # entries of the alignmentlist need to be dict's
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        json_str = json.dumps({'alignmentlist':
                               [{'target':
                                 {'name': 'target',
                                  'seqres':
                                  'APGAGKGTQAQFIMEKYGIPQISTGGGLRAAVKS---LGKQA'+
                                  'KDIMDAGKLVTDELVIALVKERIAQEDCRNGFLLDGFPRTIP'+
                                  'QADAMKEAGINVDYVLEF----ELIVDRIVGRRVHAPSGRVY'+
                                  'HVKFNPPKVEGKDDVTGEELTTRKDDQEETVRKRLVEYHQMT'+
                                  'APLL--YYYYKEAEAGNTKYAKVDGTKPVAEVRADLEKILG'},
                                 'template':
                                 {'name': '1AKE.B',
                                  'seqres':
                                  'APGAGKGTQAQFIMEKYGIPQISTGDMLRAAVKSGSELGKQA'+
                                  'KDIMDAGKLVTDELVIALVKERIAQEDCRNGFLLDGFPRTIP'+
                                  'QADAMKEAGINVDYVLEFDVPDELIVDRIVGRRVHAPSGRVY'+
                                  'HVKFNPPKVEGKDDVTGEELTTRKDDQEETVRKRLVEYHQMT'+
                                  'APLIGYYYYSKEAEAGNTKYAKVDGTKPV---AEVRADLEK',
                                  'offset': 7}}]})
        opts = parser.Parse(['--json', json_str])
        self.assertEqual(len(opts.alignments), 1)
        self.assertEqual(opts.alignments[0].GetCount(), 2)
        self.assertEqual(opts.alignments[0].GetLength(), 209)
        self.assertEqual(opts.alignments[0].GetSequenceOffset(0), 0)
        self.assertEqual(opts.alignments[0].GetSequenceOffset(1), 7)
        self.assertEqual(opts.alignments[0].GetSequence(0).gapless_string,
                         'APGAGKGTQAQFIMEKYGIPQISTGGGLRAAVKSLGKQAKDIMDAGKLVT'+
                         'DELVIALVKERIAQEDCRNGFLLDGFPRTIPQADAMKEAGINVDYVLEFE'+
                         'LIVDRIVGRRVHAPSGRVYHVKFNPPKVEGKDDVTGEELTTRKDDQEETV'+
                         'RKRLVEYHQMTAPLLYYYYKEAEAGNTKYAKVDGTKPVAEVRADLEKILG')
        self.assertEqual(opts.alignments[0].GetSequenceRole(0), 'TARGET')
        self.assertEqual(opts.alignments[0].GetSequenceRole(1), 'TEMPLATE')
        self.assertEqual(opts.alignments[0].GetSequence(0).name, 'target')
        self.assertEqual(opts.alignments[0].GetSequence(1).name, '1AKE.B')
        self.assertEqual(str(opts.alignments[0]),
                         'target  APGAGKGTQAQFIMEKYGIPQISTGGGLRAAVKS---LGKQAK'+
                         'DIMDAGKLVTDELVIALVKERIAQEDCRN\n1AKE.B  APGAGKGTQAQF'+
                         'IMEKYGIPQISTGDMLRAAVKSGSELGKQAKDIMDAGKLVTDELVIALVKE'+
                         'RIAQEDCRN\n\ntarget  GFLLDGFPRTIPQADAMKEAGINVDYVLEF'+
                         '----ELIVDRIVGRRVHAPSGRVYHVKFNPPKVEGKDDVTGE\n1AKE.B '+
                         ' GFLLDGFPRTIPQADAMKEAGINVDYVLEFDVPDELIVDRIVGRRVHAPS'+
                         'GRVYHVKFNPPKVEGKDDVTGE\n\ntarget  ELTTRKDDQEETVRKRL'+
                         'VEYHQMTAPLL--YYYYKEAEAGNTKYAKVDGTKPVAEVRADLEKILG\n1'+
                         'AKE.B  ELTTRKDDQEETVRKRLVEYHQMTAPLIGYYYYSKEAEAGNTKY'+
                         'AKVDGTKPV---AEVRADLEK\n')
        self.assertEqual(len(opts.aln_sources), 1)
        self.assertEqual(opts.aln_sources[0], json_str)

    def testAddAlignmentJsonAlnMultiString(self):
        # test multiple alignments
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        json_str = json.dumps({'alignmentlist':
                               [{'target':
                                 {'name': ' target 1',
                                  'seqres':
                                  'VLSPADKTNVKAAWGKVGAHAGEYGAEALERMFLSFPTTKTY'+
                                  'FPHF-DL-S----HGSAQVKGHGKKVADALTNAVAHVDDMPN'+
                                  'ALSALSDLHAHK-LRVDPVNFKLLSHCLLVTLAAHLPAEFTP'+
                                  'AVHASLDKFLASVSTVLTSKYR'},
                                 'template':
                                 {'name': '3e7b90809bd446a5',
                                  'seqres':
                                  'VLSEGEWQLVLHVWAKVEADVAGHGQDILIRLFKSHPETLEK'+
                                  'FDRFKHLKTEAEMKASEDLKKHGVTVLTALGAILKKKGHHEA'+
                                  'ELKPLAQSHA-TKHKIPIKYLEFISEAIIHVLHSRHPGDFGA'+
                                  'DAQGAMNKALELFRKDIAAKYK',
                                  'offset': 1}},
                                {'target':
                                 {'name': 'target 2',
                                  'seqres':
                                  'VLSPADKTNVKAAWGKVGAHAGEYGAEALERMFLSFPTTKTY'+
                                  'FPHFDLSHGSAQVKGHGKKVADALTNAVAHVDDMPNALSALS'+
                                  'DLHAHKLRVDPVNFKLLSHCLLVTLAAHLPAEFTPAVHASLD'+
                                  'KFLASVSTVLTSKYR'},
                                 'template':
                                 {'name': 'af828e69a5f2d0fd',
                                  'seqres':
                                  'VLSPADKTNVKAAWGKVGAHAGEYGAEALERMFLSFPTTKTY'+
                                  'FPHFDLSHGSAQVKGHGKKVADALTNAVAHVDDMPNALSALS'+
                                  'DLHAHKLRVDPVNFKLLSHCLLVTLAAHLPAEFTPAVHASLD'+
                                  'KFLASVSTVLTSKYR',
                                  'offset': 2}},
                                {'target':
                                 {'name': 'target 3',
                                  'seqres':
                                  'VLSPADKTNVKAAWGKVGAHAGEYGAEALERMFLSFPTTKTY'+
                                  'FPHF-DLS-----HGSAQVKGHGKKVADALTNAVAHVDDMPN'+
                                  'ALSALSDLHAHK-LRVDPVNFKLLSHCLLVTLAAHLPAEFTP'+
                                  'AVHASLDKFLASVSTVLTSKYR'},
                                 'template':
                                 {'name': '9287755aa6aa2758',
                                  'seqres':
                                  'HLTPEEKSAVTALWGKVN--VDEVGGEALGRLLVVYPWTQRF'+
                                  'FESFGDLSTPDAVMGNPKVKAHGKKVLGAFSDGLAHLDNLKG'+
                                  'TFATLSELHC-DKLHVDPENFRLLGNVLVCVLAHHFGKEFTP'+
                                  'PVQAAYQKVVAGVANALAHKYH',
                                  'offset': 3}},
                                {'target': {'name': 'target 4',
                                            'seqres': 'VDPVNFKLLSHCLLVTLAAHL'},
                                 'template': {'name': 'e69e1ac0a4b2554d',
                                              'seqres': 'ATPEQAQLVHKEIRKIVKDTC',
                                              'offset': 4}}]})
        opts = parser.Parse(['--json', json_str])

        self.assertEqual(len(opts.aln_sources), 1)
        self.assertEqual(opts.aln_sources[0],json_str)
        self.assertEqual(len(opts.alignments), 4)
        # aln 1
        self.assertEqual(opts.alignments[0].GetCount(), 2)
        self.assertEqual(opts.alignments[0].GetLength(), 148)
        self.assertEqual(opts.alignments[0].GetSequenceOffset(0), 0)
        self.assertEqual(opts.alignments[0].GetSequenceOffset(1), 1)
        self.assertEqual(opts.alignments[0].GetSequenceRole(0), 'TARGET')
        self.assertEqual(opts.alignments[0].GetSequenceRole(1), 'TEMPLATE')
        self.assertEqual(opts.alignments[0].GetSequence(0).name, 'target 1')
        self.assertEqual(opts.alignments[0].GetSequence(1).name,
                         '3e7b90809bd446a5')
        self.assertEqual(str(opts.alignments[0]),
                         'target 1          VLSPADKTNVKAAWGKVGAHAGEYGAEALERMF'+
                         'LSFPTTKTYFPHF-DL-S----HGSAQVK\n3e7b90809bd446a5  VL'+
                         'SEGEWQLVLHVWAKVEADVAGHGQDILIRLFKSHPETLEKFDRFKHLKTEA'+
                         'EMKASEDLK\n\ntarget 1          GHGKKVADALTNAVAHVDDM'+
                         'PNALSALSDLHAHK-LRVDPVNFKLLSHCLLVTLAAHLPAEF\n3e7b908'+
                         '09bd446a5  KHGVTVLTALGAILKKKGHHEAELKPLAQSHA-TKHKIPI'+
                         'KYLEFISEAIIHVLHSRHPGDF\n\ntarget 1          TPAVHAS'+
                         'LDKFLASVSTVLTSKYR\n3e7b90809bd446a5  GADAQGAMNKALEL'+
                         'FRKDIAAKYK\n')

        # aln 2
        self.assertEqual(opts.alignments[1].GetCount(), 2)
        self.assertEqual(opts.alignments[1].GetLength(), 141)
        self.assertEqual(opts.alignments[1].GetSequenceOffset(0), 0)
        self.assertEqual(opts.alignments[1].GetSequenceOffset(1), 2)
        self.assertEqual(opts.alignments[1].GetSequenceRole(0), 'TARGET')
        self.assertEqual(opts.alignments[1].GetSequenceRole(1), 'TEMPLATE')
        self.assertEqual(opts.alignments[1].GetSequence(0).name, 'target 2')
        self.assertEqual(opts.alignments[1].GetSequence(1).name,
                         'af828e69a5f2d0fd')
        self.assertEqual(str(opts.alignments[1]),
                         'target 2          VLSPADKTNVKAAWGKVGAHAGEYGAEALERMF'+
                         'LSFPTTKTYFPHFDLSHGSAQVKGHGKKV\naf828e69a5f2d0fd  VL'+
                         'SPADKTNVKAAWGKVGAHAGEYGAEALERMFLSFPTTKTYFPHFDLSHGSA'+
                         'QVKGHGKKV\n\ntarget 2          ADALTNAVAHVDDMPNALSA'+
                         'LSDLHAHKLRVDPVNFKLLSHCLLVTLAAHLPAEFTPAVHAS\naf828e6'+
                         '9a5f2d0fd  ADALTNAVAHVDDMPNALSALSDLHAHKLRVDPVNFKLLS'+
                         'HCLLVTLAAHLPAEFTPAVHAS\n\ntarget 2          LDKFLAS'+
                         'VSTVLTSKYR\naf828e69a5f2d0fd  LDKFLASVSTVLTSKYR\n')
        # aln 3
        self.assertEqual(opts.alignments[2].GetCount(), 2)
        self.assertEqual(opts.alignments[2].GetLength(), 148)
        self.assertEqual(opts.alignments[2].GetSequenceOffset(0), 0)
        self.assertEqual(opts.alignments[2].GetSequenceOffset(1), 3)
        self.assertEqual(opts.alignments[2].GetSequenceRole(0), 'TARGET')
        self.assertEqual(opts.alignments[2].GetSequenceRole(1), 'TEMPLATE')
        self.assertEqual(opts.alignments[2].GetSequence(0).name, 'target 3')
        self.assertEqual(opts.alignments[2].GetSequence(1).name,
                         '9287755aa6aa2758')
        self.assertEqual(str(opts.alignments[2]),
                         'target 3          VLSPADKTNVKAAWGKVGAHAGEYGAEALERMF'+
                         'LSFPTTKTYFPHF-DLS-----HGSAQVK\n9287755aa6aa2758  HL'+
                         'TPEEKSAVTALWGKVN--VDEVGGEALGRLLVVYPWTQRFFESFGDLSTPD'+
                         'AVMGNPKVK\n\ntarget 3          GHGKKVADALTNAVAHVDDM'+
                         'PNALSALSDLHAHK-LRVDPVNFKLLSHCLLVTLAAHLPAEF\n9287755'+
                         'aa6aa2758  AHGKKVLGAFSDGLAHLDNLKGTFATLSELHC-DKLHVDP'+
                         'ENFRLLGNVLVCVLAHHFGKEF\n\ntarget 3          TPAVHAS'+
                         'LDKFLASVSTVLTSKYR\n9287755aa6aa2758  TPPVQAAYQKVVAG'+
                         'VANALAHKYH\n')
        # aln 4
        self.assertEqual(opts.alignments[3].GetCount(), 2)
        self.assertEqual(opts.alignments[3].GetLength(), 21)
        self.assertEqual(opts.alignments[3].GetSequenceOffset(0), 0)
        self.assertEqual(opts.alignments[3].GetSequenceOffset(1), 4)
        self.assertEqual(opts.alignments[3].GetSequenceRole(0), 'TARGET')
        self.assertEqual(opts.alignments[3].GetSequenceRole(1), 'TEMPLATE')
        self.assertEqual(opts.alignments[3].GetSequence(0).name, 'target 4')
        self.assertEqual(opts.alignments[3].GetSequence(1).name,
                         'e69e1ac0a4b2554d')
        self.assertEqual(str(opts.alignments[3]),
                         'target 4          VDPVNFKLLSHCLLVTLAAHL\ne69e1ac0a4'+
                         'b2554d  ATPEQAQLVHKEIRKIVKDTC\n')

    def testAddAlignmentJsonWorkingGzipFile(self):
        # positive test: everything works!
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        json_obj = _GetJsonTestObj()
        tmp_json = tempfile.NamedTemporaryFile(suffix='.json.gz')
        with gzip.open(tmp_json.name, 'wt') as gfh:
            json.dump(json_obj, gfh)
        tmp_json.flush()
        opts = parser.Parse(['--json', tmp_json.name])
        self.assertEqual(len(opts.aln_sources), 1)
        self.assertEqual(opts.aln_sources[0], tmp_json.name)
        self.assertEqual(len(opts.alignments), 1)
        self.assertEqual(str(opts.alignments[0]),
                         'TARGET    VLSPADKTNVKAAWGKVGAHAGEYGAEALERMFLSFPTTKT'+
                         'YFPHF-DLS-----HGSAQVKGHGKKVAD\nTEMPLATE  HLTPEEKSAV'+
                         'TALWGKVN--VDEVGGEALGRLLVVYPWTQRFFESFGDLSTPDAVMGNPKV'+
                         'KAHGKKVLG\n\nTARGET    ALTNAVAHVDDMPNALSALSDLHAHK-L'+
                         'RVDPVNFKLLSHCLLVTLAAHLPAEFTPAVHASLDKFLASVS\nTEMPLAT'+
                         'E  AFSDGLAHLDNLKGTFATLSELHC-DKLHVDPENFRLLGNVLVCVLAH'+
                         'HFGKEFTPPVQAAYQKVVAGVA\n\nTARGET    TVLTSKYR\nTEMPL'+
                         'ATE  NALAHKYH\n')

    def testAddAlignmentJsonWorkingFile(self):
        # positive test: everything works!
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        json_obj = _GetJsonTestObj()
        tmp_json = tempfile.NamedTemporaryFile(suffix='.json', mode='w')
        json.dump(json_obj, tmp_json)
        tmp_json.flush()
        opts = parser.Parse(['--json', tmp_json.name])
        self.assertEqual(len(opts.aln_sources), 1)
        self.assertEqual(opts.aln_sources[0], tmp_json.name)
        self.assertEqual(len(opts.alignments), 1)
        self.assertEqual(str(opts.alignments[0]),
                         'TARGET    VLSPADKTNVKAAWGKVGAHAGEYGAEALERMFLSFPTTKT'+
                         'YFPHF-DLS-----HGSAQVKGHGKKVAD\nTEMPLATE  HLTPEEKSAV'+
                         'TALWGKVN--VDEVGGEALGRLLVVYPWTQRFFESFGDLSTPDAVMGNPKV'+
                         'KAHGKKVLG\n\nTARGET    ALTNAVAHVDDMPNALSALSDLHAHK-L'+
                         'RVDPVNFKLLSHCLLVTLAAHLPAEFTPAVHASLDKFLASVS\nTEMPLAT'+
                         'E  AFSDGLAHLDNLKGTFATLSELHC-DKLHVDPENFRLLGNVLVCVLAH'+
                         'HFGKEFTPPVQAAYQKVVAGVA\n\nTARGET    TVLTSKYR\nTEMPL'+
                         'ATE  NALAHKYH\n')

    def testAddAlignmentJsonMulti(self):
        # passing --json multiple times is not allowed
        # also serves as a unit test for PM3StoreOnceAction
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        json_obj = _GetJsonTestObj()
        tmp_json = tempfile.NamedTemporaryFile(suffix='.json', mode='w')
        json.dump(json_obj, tmp_json)
        tmp_json.flush()
        json_str = json.dumps(json_obj)
        opts = parser.Parse(['--json', tmp_json.name,
                             '--json', json_str])
        self.assertEqual(len(opts.aln_sources), 2)
        self.assertEqual(opts.aln_sources[0], tmp_json.name)
        self.assertEqual(opts.aln_sources[1], json_str)
        self.assertEqual(len(opts.alignments), 2)
        self.assertEqual(str(opts.alignments[0]),
                         'TARGET    VLSPADKTNVKAAWGKVGAHAGEYGAEALERMFLSFPTTKT'+
                         'YFPHF-DLS-----HGSAQVKGHGKKVAD\nTEMPLATE  HLTPEEKSAV'+
                         'TALWGKVN--VDEVGGEALGRLLVVYPWTQRFFESFGDLSTPDAVMGNPKV'+
                         'KAHGKKVLG\n\nTARGET    ALTNAVAHVDDMPNALSALSDLHAHK-L'+
                         'RVDPVNFKLLSHCLLVTLAAHLPAEFTPAVHASLDKFLASVS\nTEMPLAT'+
                         'E  AFSDGLAHLDNLKGTFATLSELHC-DKLHVDPENFRLLGNVLVCVLAH'+
                         'HFGKEFTPPVQAAYQKVVAGVA\n\nTARGET    TVLTSKYR\nTEMPL'+
                         'ATE  NALAHKYH\n')
        self.assertEqual(str(opts.alignments[0]), str(opts.alignments[1]))

    def testAddAlignmentClustal(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AssembleParser()
        opts = parser.Parse(['--clustal', 'data/aln_tpl/2jlp-1.aln',
                             '-c', 'data/aln_tpl/2jlp-1.aln'])
        self.assertEqual(str(opts.alignments[0]),
                         'TARGET  MVVKAVCVINGDAKGTVFFEQESSGTPVKVSGEVCGLAKGLHG'+
                         'FHVHEFGDNTNGCMSSGPHFNPYGKEHGA\nA|55    ------------'+
                         '-----------------------------RAIHVHQFGDLSQGCESTGPHY'+
                         'NPLAVPH--\n\nTARGET  PVDENRHLGDLGNIEATGDCPTKVNITDSK'+
                         'ITLFGADSIIGRTVVVHADADDLGQGGHELSKSTGNAGARIG\nA|55   '+
                         ' ----PQHPGDFGNF-AVRDGSLWRYRAGLAASLAGPHSIVGRAVVVHAGE'+
                         'DDLGRGGNQASVENGNAGRRLA\n\nTARGET  CGVIGIAKV\nA|55  '+
                         '  CCVVGV---\n')
        self.assertEqual(str(opts.alignments[0]), str(opts.alignments[1]))

    def testStructuresPDB(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddStructure()
        parser.AssembleParser()
        opts = parser.Parse(['--pdb', 'data/aln_tpl/1crn_cut.pdb',
                             '-p', 'data/aln_tpl/2aoh-1_cut.pdb'])
        self.assertEqual(len(opts.structure_sources), 2)
        self.assertEqual(opts.structure_sources[0], 'data/aln_tpl/1crn_cut.pdb')
        self.assertEqual(opts.structure_sources[1], 'data/aln_tpl/2aoh-1_cut.pdb')
        self.assertEqual(len(opts.structures), len(opts.structure_sources))
        self.assertEqual(opts.structures[0].chain_count, 1)
        self.assertEqual(opts.structures[0].residue_count, 40)
        self.assertEqual(opts.structures[1].chain_count, 2)
        self.assertEqual(opts.structures[1].residue_count, 186)

    def testStructuresEntity(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddStructure()
        parser.AssembleParser()
        opts = parser.Parse(['--entity', 'data/aln_tpl/2jlp-1.pdb',
                             '-e', 'data/aln_tpl/5d52-1_cut.pdb'])
        self.assertEqual(len(opts.structure_sources), 2)
        self.assertEqual(opts.structure_sources[0], 'data/aln_tpl/2jlp-1.pdb')
        self.assertEqual(opts.structure_sources[1], 'data/aln_tpl/5d52-1_cut.pdb')
        self.assertEqual(len(opts.structures), len(opts.structure_sources))
        self.assertEqual(opts.structures[0].chain_count, 6)
        self.assertEqual(opts.structures[0].residue_count, 1569)
        self.assertEqual(opts.structures[1].chain_count, 2)
        self.assertEqual(opts.structures[1].residue_count, 39)

    def testStructuresNoFile(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddStructure()
        parser.AssembleParser()
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--pdb', 'data/missingfile.pdb'])
        self.assertEqual(ecd.exception.code, 32)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'],
                         ['PDB Structure file does not exist: data/missingfile.pdb'])

    def testStructuresFailToRead(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddStructure()
        parser.AssembleParser()
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--pdb', 'data/aln_tpl/2jlp-1.aln'])
        self.assertEqual(ecd.exception.code, 33)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        exp_str = "'--pdb data/aln_tpl/2jlp-1.aln': failure to parse PDB file"
        self.assertTrue(self.log.messages['ERROR'][0].startswith(exp_str))

    def testStructuresNoFormat(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddStructure()
        parser.AssembleParser()
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['--entity', 'data/aln_tpl/2jlp-1.aln'])
        self.assertEqual(ecd.exception.code, 34)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        exp_str = "'--entity data/aln_tpl/2jlp-1.aln': not a supported format"
        self.assertTrue(self.log.messages['ERROR'][0].startswith(exp_str))

    def testGetChains(self):
        # test internal _GetChains function as it's important
        tpl_single_chain = ost.io.LoadPDB('data/aln_tpl/1crn_cut.pdb')
        tpl_many_chains = ost.io.LoadPDB('data/aln_tpl/2jlp-1.pdb')
        chain_entities = pm3argparse._GetChains([tpl_single_chain], ['a/b.pdb'])
        # single
        exp_keys = ['UNIQUE']
        self.assertEqual(sorted(chain_entities.keys()), exp_keys)
        chain_entities = pm3argparse._GetChains([tpl_many_chains], ['a/2.pdb'])
        exp_keys = ['2.A', '2.B', '2.C', '2.D', 'A', 'B', 'C', 'D']
        self.assertEqual(sorted(chain_entities.keys()), exp_keys)
        chain_entities = pm3argparse._GetChains([tpl_many_chains], ['2.pdb.gz'])
        exp_keys = ['2.A', '2.B', '2.C', '2.D', 'A', 'B', 'C', 'D']
        self.assertEqual(sorted(chain_entities.keys()), exp_keys)
        # combine
        structures = [tpl_single_chain, tpl_many_chains, tpl_single_chain]
        sources = ['a/1.pdb', 'd/2.pdb', '3.pdb.gz']
        chain_entities = pm3argparse._GetChains(structures, sources)
        exp_keys = ['1', '1.A', '2.A', '2.B', '2.C', '2.D', '3', '3.A']
        self.assertEqual(sorted(chain_entities.keys()), exp_keys)

    def checkView(self, alns):
        # helper: checks if all alignments have attached views for templates
        for aln in alns:
            for i in range(1, aln.GetCount()):
                self.assertTrue(aln.GetSequence(i).HasAttachedView())
                for col in aln:
                    res = col.GetResidue(i)
                    all_good = (res.IsValid() and res.one_letter_code == col[i])\
                               or (not res.IsValid() and '-' == col[i])
                    mymsg = "mismatch col. " + str(col) + " with res. " + str(res)
                    self.assertTrue(all_good, msg=mymsg)

    def testAttachView(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AddStructure(attach_views=True)
        parser.AssembleParser()
        opts = parser.Parse(['-f', 'data/aln_tpl/1crn.fasta',
                             '-e', 'data/aln_tpl/1crn_cut.pdb'])
        self.assertEqual(len(opts.alignments), 1)
        self.checkView(opts.alignments)

    def testAttachViewHomo(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AddStructure(attach_views=True)
        parser.AssembleParser()
        opts = parser.Parse(['-f', 'data/aln_tpl/2aoh-1_cut_A.fasta',
                             '-p', 'data/aln_tpl/2aoh-1_cut.pdb',
                             '-f', 'data/aln_tpl/2aoh-1_cut_B.fasta'])
        self.assertEqual(len(opts.alignments), 2)
        self.checkView(opts.alignments)

    def testAttachViewOffset(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AddStructure(attach_views=True)
        parser.AssembleParser()
        opts = parser.Parse(['-p', 'data/aln_tpl/2jlp-1.pdb',
                             '-f', 'data/aln_tpl/2jlp-1.fasta'])
        self.assertEqual(len(opts.alignments), 1)
        self.checkView(opts.alignments)

    def testAttachViewOffsetClustal(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AddStructure(attach_views=True)
        parser.AssembleParser()
        opts = parser.Parse(['-p', 'data/aln_tpl/2jlp-1.pdb',
                             '-c', 'data/aln_tpl/2jlp-1.aln'])
        self.assertEqual(len(opts.alignments), 1)
        self.checkView(opts.alignments)

    def testAttachViewOffsetJSON(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AddStructure(attach_views=True)
        parser.AssembleParser()
        opts = parser.Parse(['-p', 'data/aln_tpl/2jlp-1.pdb',
                             '-j', 'data/aln_tpl/2jlp-1.json'])
        self.assertEqual(len(opts.alignments), 1)
        self.checkView(opts.alignments)

    def testAttachViewHetero(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AddStructure(attach_views=True)
        parser.AssembleParser()
        opts = parser.Parse(['-p', 'data/aln_tpl/5d52-1_cut.pdb',
                             '-f', 'data/aln_tpl/5d52-1_cut_A.fasta',
                             '-f', 'data/aln_tpl/5d52-1_cut_B.fasta'])
        self.assertEqual(len(opts.alignments), 2)
        self.checkView(opts.alignments)

    def testAttachViewNoAln(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddStructure(attach_views=True)
        parser.AssembleParser()
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['-p', 'data/aln_tpl/1crn_cut.pdb'])
        self.assertEqual(ecd.exception.code, 41)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        exp_str = "Need to have structures and alignments to attach views."
        self.assertEqual(self.log.messages['ERROR'], [exp_str])

    def testAttachViewBadOffsets(self):
        # this may happen with JSON input by doubly defining offsets
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AddStructure(attach_views=True)
        parser.AssembleParser()
        # mess up JSON input
        with open('data/aln_tpl/2jlp-1.json') as jfh:
            json_obj = json.load(jfh)
        json_obj['alignmentlist'][0]['template']['name'] = 'A|54'
        json_str = json.dumps(json_obj)
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['-p', 'data/aln_tpl/2jlp-1.pdb',
                          '-j', json_str])
        self.assertEqual(ecd.exception.code, 42)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'],
                         ['Inconsistent offsets between seq. name and seq. '+
                          'in alignment for A|54'])

    def testAttachViewNonIntegerOffset(self):
        # this may happen with JSON input by doubly defining offsets
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AddStructure(attach_views=True)
        parser.AssembleParser()
        # mess up JSON input
        with open('data/aln_tpl/2jlp-1.json') as jfh:
            json_obj = json.load(jfh)
        json_obj['alignmentlist'][0]['template']['name'] = 'A|5a5'
        json_str = json.dumps(json_obj)
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['-p', 'data/aln_tpl/2jlp-1.pdb',
                          '-j', json_str])
        self.assertEqual(ecd.exception.code, 43)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'],
                         ['Non-integer offset defined in seq. name A|5a5'])

    def testAttachViewTooManyBars(self):
        # this may happen with JSON input by doubly defining offsets
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AddStructure(attach_views=True)
        parser.AssembleParser()
        # mess up JSON input
        with open('data/aln_tpl/2jlp-1.json') as jfh:
            json_obj = json.load(jfh)
        json_obj['alignmentlist'][0]['template']['name'] = 'A|B|55'
        json_str = json.dumps(json_obj)
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['-p', 'data/aln_tpl/2jlp-1.pdb',
                          '-j', json_str])
        self.assertEqual(ecd.exception.code, 44)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'],
                         ["Too many '|' in seq. name A|B|55"])

    def testAttachViewUnknownChain(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AddStructure(attach_views=True)
        parser.AssembleParser()
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['-f', 'data/aln_tpl/1crn.fasta',
                          '-p', 'data/aln_tpl/1crn_cut.pdb',
                          '-p', 'data/aln_tpl/2jlp-1.pdb'])
        self.assertEqual(ecd.exception.code, 45)
        self.assertEqual(len(self.log.messages['ERROR']), 1)
        self.assertEqual(self.log.messages['ERROR'],
                         ['Could not find chain with ID tpl (should be '+
                          '<FILE>.<CHAIN>) to attach to sequence named tpl'])  

    def testProfileDoesNotExist(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AddProfile()
        parser.AssembleParser()
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['-f', 'data/aln_tpl/1crn.fasta',
                          '-s', 'data/aln_tpl/IDoNotExist.hhm'])
        self.assertEqual(ecd.exception.code, 51)
        self.assertEqual(len(self.log.messages['ERROR']), 1)

    def testProfileIsFuckedUp(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AddProfile()
        parser.AssembleParser()
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['-f', 'data/aln_tpl/1crn.fasta',
                          '-s', 'data/aln_tpl/1crn_corrupted.hhm'])
        self.assertEqual(ecd.exception.code, 52)
        self.assertEqual(len(self.log.messages['ERROR']), 1)

    def testAdditionalProfile(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AddProfile()
        parser.AssembleParser()
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['-f', 'data/aln_tpl/1crn.fasta',
                          '-s', 'data/aln_tpl/1crn.hhm',
                          '-s', 'data/aln_tpl/5ua4_B.hhm'])
        self.assertEqual(ecd.exception.code, 53)
        self.assertEqual(len(self.log.messages['ERROR']), 1)

    def testUniqueProfiles(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AddProfile()
        parser.AssembleParser()
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['-f', 'data/aln_tpl/1crn.fasta',
                          '-s', 'data/aln_tpl/1crn.hhm',
                          '-s', 'data/aln_tpl/1crn.hhm'])
        self.assertEqual(ecd.exception.code, 54)
        self.assertEqual(len(self.log.messages['ERROR']), 1)

    def testAllCovered(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AddProfile()
        parser.AssembleParser()
        with self.assertRaises(SystemExit) as ecd:
            parser.Parse(['-f', 'data/aln_tpl/1crn.fasta',
                          '-f', 'data/aln_tpl/5d52-1_cut_A.fasta',
                          '-s', 'data/aln_tpl/1crn.hhm'])
        self.assertEqual(ecd.exception.code, 55)
        self.assertEqual(len(self.log.messages['ERROR']), 1)

    def testAllGoodOneChain(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AddProfile()
        parser.AssembleParser()

        opts =  parser.Parse(['-f', 'data/aln_tpl/1crn.fasta',
                              '-s', 'data/aln_tpl/1crn.hhm'])

        self.assertEqual(len(opts.alignments), 1)
        self.assertEqual(len(opts.profiles), 1)
        self.assertEqual(opts.alignments[0].GetSequence(0).GetGaplessString(),
                         opts.profiles[0].sequence)

    def testAllGoodHomoOligo(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AddProfile()
        parser.AssembleParser()

        opts =  parser.Parse(['-f', 'data/aln_tpl/1crn.fasta',
                              '-f', 'data/aln_tpl/1crn.fasta',
                              '-s', 'data/aln_tpl/1crn.hhm'])
        self.assertEqual(len(opts.alignments), 2)
        self.assertEqual(len(opts.profiles), 2)
        self.assertEqual(opts.alignments[0].GetSequence(0).GetGaplessString(),
                         opts.profiles[0].sequence)
        self.assertEqual(opts.alignments[1].GetSequence(0).GetGaplessString(),
                         opts.profiles[1].sequence)

    def testAllGoodHeteroOligo(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AddProfile()
        parser.AssembleParser()

        opts =  parser.Parse(['-f', 'data/aln_tpl/1crn.fasta',
                              '-f', 'data/aln_tpl/5ua4_B.fasta',
                              '-s', 'data/aln_tpl/1crn.hhm',
                              '-s', 'data/aln_tpl/5ua4_B.hhm'])
        self.assertEqual(len(opts.alignments), 2)
        self.assertEqual(len(opts.profiles), 2)
        self.assertEqual(opts.alignments[0].GetSequence(0).GetGaplessString(),
                         opts.profiles[0].sequence)
        self.assertEqual(opts.alignments[1].GetSequence(0).GetGaplessString(),
                         opts.profiles[1].sequence)


    def testFraggerNotRequested(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AddFragments()
        parser.AssembleParser()
        opts =  parser.Parse(['-f', 'data/aln_tpl/1crn.fasta',
                              '-f', 'data/aln_tpl/5ua4_B.fasta'])
        self.assertEqual(len(opts.alignments), 2)
        self.assertEqual(len(opts.fragger_handles), 0)

    def testFraggerAttachedWithoutProfile(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AddFragments()
        parser.AssembleParser()
        opts =  parser.Parse(['-f', 'data/aln_tpl/1crn.fasta',
                              '-f', 'data/aln_tpl/5ua4_B.fasta',
                              '--use-fragments'])
        self.assertEqual(len(opts.alignments), 2)
        self.assertEqual(len(opts.fragger_handles), 2)
        self.assertEqual(opts.alignments[0].GetSequence(0).GetGaplessString(),
                         opts.fragger_handles[0].sequence)
        self.assertEqual(opts.alignments[1].GetSequence(0).GetGaplessString(),
                         opts.fragger_handles[1].sequence)
        # most of the stuff in the fragger handles should be None
        self.assertIsNone(opts.fragger_handles[0].profile)
        self.assertIsNone(opts.fragger_handles[0].psipred_pred)
        self.assertIsNone(opts.fragger_handles[0].torsion_sampler_coil)
        self.assertIsNone(opts.fragger_handles[0].torsion_sampler_helix)
        self.assertIsNone(opts.fragger_handles[0].torsion_sampler_extended)
        self.assertIsNone(opts.fragger_handles[1].profile)
        self.assertIsNone(opts.fragger_handles[1].psipred_pred)
        self.assertIsNone(opts.fragger_handles[1].torsion_sampler_coil)
        self.assertIsNone(opts.fragger_handles[1].torsion_sampler_helix)
        self.assertIsNone(opts.fragger_handles[1].torsion_sampler_extended)

    def testFraggerAttachedWithProfile(self):
        parser = pm3argparse.PM3ArgumentParser(__doc__, action=False)
        parser.AddAlignment()
        parser.AddProfile()
        parser.AddFragments()
        parser.AssembleParser()
        opts =  parser.Parse(['-f', 'data/aln_tpl/1crn.fasta',
                              '-f', 'data/aln_tpl/5ua4_B.fasta',
                              '-s', 'data/aln_tpl/1crn.hhm',
                              '-s', 'data/aln_tpl/5ua4_B.hhm',
                              '--use-fragments'])
        self.assertEqual(len(opts.alignments), 2)
        self.assertEqual(len(opts.fragger_handles), 2)
        self.assertEqual(opts.alignments[0].GetSequence(0).GetGaplessString(),
                         opts.fragger_handles[0].sequence)
        self.assertEqual(opts.alignments[1].GetSequence(0).GetGaplessString(),
                         opts.fragger_handles[1].sequence)
        # most of the stuff in the fragger handles should be set
        self.assertIsNotNone(opts.fragger_handles[0].profile)
        self.assertIsNotNone(opts.fragger_handles[0].psipred_pred)
        self.assertIsNotNone(opts.fragger_handles[0].torsion_sampler_coil)
        self.assertIsNotNone(opts.fragger_handles[0].torsion_sampler_helix)
        self.assertIsNotNone(opts.fragger_handles[0].torsion_sampler_extended)
        self.assertIsNotNone(opts.fragger_handles[1].profile)
        self.assertIsNotNone(opts.fragger_handles[1].psipred_pred)
        self.assertIsNotNone(opts.fragger_handles[1].torsion_sampler_coil)
        self.assertIsNotNone(opts.fragger_handles[1].torsion_sampler_helix)
        self.assertIsNotNone(opts.fragger_handles[1].torsion_sampler_extended)

# test options: --disable-aln check (for amino acids)
# test options: --disable-input-checks (for all)
# test option: --disable-mm-check (macromolecule)

if __name__ == "__main__":
    from ost import testutils
    testutils.RunTests()
