Example #1
0
 def test_access_optional_scalar(self):
     frozen = self._freeze(reaction_pb2.Percentage(value=12.3))
     self.assertTrue(hasattr(frozen, 'value'))
     self.assertAlmostEqual(frozen.value, 12.3, places=6)
     self.assertFalse(hasattr(frozen, 'precision'))
     with self.assertRaises(AttributeError):
         _ = frozen.precision
Example #2
0
class ServeTest(parameterized.TestCase, absltest.TestCase):
    def _create(self, dataset):
        """Make an empty dataset for testing."""
        response = self.client.post(f'/dataset/{dataset}/new',
                                    follow_redirects=True)
        self.assertEqual(response.status_code, 200)

    def _destroy(self, dataset):
        """Clean up a dataset that was created for testing."""
        response = self.client.get(f'/dataset/{dataset}/delete',
                                   follow_redirects=True)
        # Returns 200 even if the dataset did not exist before.
        self.assertEqual(response.status_code, 200)

    def _destroy_datasets(self):
        for dataset in DATASETS:
            self._destroy(dataset)

    def setUp(self):
        super().setUp()
        self.test_directory = self.create_tempdir()
        serve.app.config['TESTING'] = True
        self.client = serve.app.test_client()
        # GET requests automatically login as the test user.
        self.client.get('/authenticate')
        self.testdata = os.path.join(
            os.path.dirname(os.path.realpath(__file__)), 'testdata')
        self._destroy_datasets()
        # Start with an initial empty dataset called 'dataset'.
        self._create('dataset')

    def _get_dataset(self):
        """Returns a Dataset for testing."""
        dataset = dataset_pb2.Dataset()
        with open(os.path.join(self.testdata,
                               'nielsen_fig1_dataset.pbtxt')) as f:
            text_format.Parse(f.read(), dataset)
        # Add some unicode to check for encoding/decoding robustness.
        # From https://en.wikipedia.org/wiki/Atlantis.
        dataset.reactions[0].provenance.city = 'Ἀτλαντὶς νῆσος'
        return dataset

    def _download_dataset(self, name):
        """Downloads an existing dataset."""
        response = self.client.get(f'/dataset/{name}/download',
                                   follow_redirects=True)
        self.assertEqual(response.status_code, 200)
        return dataset_pb2.Dataset.FromString(response.data)

    def _upload_dataset(self, dataset, name):
        """Uploads a Dataset for testing."""
        response = self.client.post(f'/dataset/{name}/upload',
                                    data=text_format.MessageToString(dataset),
                                    follow_redirects=True)
        self.assertEqual(response.status_code, 200)

    def test_show_root(self):
        response = self.client.get('/', follow_redirects=True)
        self.assertEqual(response.status_code, 200)

    def test_show_datasets(self):
        response = self.client.get('/datasets', follow_redirects=True)
        self.assertEqual(response.status_code, 200)

    @parameterized.parameters([
        ('dataset', 200),
        ('other', 404),
    ])
    def test_show_dataset(self, dataset, expected):
        response = self.client.get(f'/dataset/{dataset}',
                                   follow_redirects=True)
        self.assertEqual(response.status_code, expected)

    @parameterized.parameters([
        ('dataset', 200),
        ('../dataset', 404),
        (urllib.parse.quote_plus('../dataset'), 404),
        ('/foo/bar', 404),
        ('other', 404),
    ])
    def test_download_dataset(self, file_name, expected):
        response = self.client.get(f'/dataset/{file_name}/download',
                                   follow_redirects=True)
        self.assertEqual(response.status_code, expected)
        if response.status_code == 200:
            # Make sure it parses.
            filename = os.path.join(self.test_directory, 'dataset.pb')
            with open(filename, 'wb') as f:
                f.write(response.data)
            message_helpers.load_message(filename, dataset_pb2.Dataset)

    @parameterized.parameters([
        ('dataset', 'pb', 200),
        ('dataset', 'pbtxt', 200),
        ('../dataset', 'pb', 404),
        ('../dataset', 'pbtxt', 404),
        (urllib.parse.quote_plus('../dataset'), 'pb', 404),
        (urllib.parse.quote_plus('../dataset'), 'pbtxt', 404),
        ('/foo/bar', 'pb', 404),
        ('/foo/bar', 'pbtxt', 404),
        ('other', 'pb', 404),
        ('other', 'pbtxt', 404),
    ])
    def test_download_dataset_with_kind(self, file_name, kind, expected):
        response = self.client.get(f'/dataset/{file_name}/download/{kind}',
                                   follow_redirects=True)
        self.assertEqual(response.status_code, expected)
        if response.status_code == 200:
            # Make sure it parses.
            filename = os.path.join(self.test_directory, f'dataset.{kind}')
            with open(filename, 'wb') as f:
                f.write(response.data)
            message_helpers.load_message(filename, dataset_pb2.Dataset)

    @parameterized.parameters([
        ('dataset', 409, True),
        ('dataset', 409, False),
        ('other', 200, True),
        ('other', 200, False),
    ])
    def test_upload_dataset(self, file_name, expected, as_text):
        dataset = self._get_dataset()
        if as_text:
            data = text_format.MessageToString(dataset)
        else:
            data = dataset.SerializeToString()
        response = self.client.post(f'/dataset/{file_name}/upload',
                                    data=data,
                                    follow_redirects=True)
        self.assertEqual(response.status_code, expected)
        if response.status_code == 200:
            response = self.client.get(f'/dataset/{file_name}/download',
                                       follow_redirects=True)
            self.assertEqual(response.status_code, 200)
            downloaded_dataset = dataset_pb2.Dataset.FromString(response.data)
            self.assertEqual(downloaded_dataset, dataset)

    @parameterized.parameters([
        ('dataset', 409),
        ('other', 200),
    ])
    def test_new_dataset(self, file_name, expected):
        response = self.client.post(f'/dataset/{file_name}/new',
                                    follow_redirects=True)
        self.assertEqual(response.status_code, expected)
        if response.status_code == 200:
            dataset = self._download_dataset(file_name)
            self.assertEmpty(dataset.reactions)

    @parameterized.parameters([b'', b'data:foo/bar;base64,'])
    def test_enumerate_dataset(self, prefix):
        data = {'spreadsheet_name': 'test.csv'}
        with open(os.path.join(self.testdata, 'nielsen_fig1.csv'), 'rb') as f:
            data['spreadsheet_data'] = (prefix +
                                        base64.b64encode(f.read())).decode()
        with open(os.path.join(self.testdata,
                               'nielsen_fig1_template.pbtxt')) as f:
            data['template_string'] = f.read()
        response = self.client.post('/dataset/enumerate',
                                    json=data,
                                    follow_redirects=True)
        self.assertEqual(response.status_code, 200, response.data)
        response = self.client.get('/dataset/test_dataset/download',
                                   follow_redirects=True)
        self.assertEqual(response.status_code, 200)
        dataset = dataset_pb2.Dataset.FromString(response.data)
        self.assertLen(dataset.reactions, 80)

    @parameterized.parameters([
        (0, 200),
        (3, 200),
        (80, 404),
    ])
    def test_show_reaction(self, index, expected):
        self._upload_dataset(self._get_dataset(), 'test')
        response = self.client.get(f'/dataset/test/reaction/{index}',
                                   follow_redirects=True)
        self.assertEqual(response.status_code, expected)

    def test_download_reaction(self):
        reaction = self._get_dataset().reactions[0]
        response = self.client.post('/reaction/download',
                                    data=reaction.SerializeToString(),
                                    follow_redirects=True)
        self.assertEqual(response.status_code, 200)
        downloaded_reaction = reaction_pb2.Reaction()
        text_format.Parse(response.data, downloaded_reaction)
        self.assertEqual(downloaded_reaction, reaction)

    def test_new_reaction(self):
        name = 'test'
        dataset = self._get_dataset()
        self._upload_dataset(dataset, name)
        response = self.client.get(f'/dataset/{name}/new/reaction',
                                   follow_redirects=True)
        self.assertEqual(response.status_code, 200)
        downloaded_dataset = self._download_dataset(name)
        self.assertLen(downloaded_dataset.reactions, 81)

    def test_clone_reaction(self):
        name = 'test'
        dataset = self._get_dataset()
        self._upload_dataset(dataset, name)
        response = self.client.get(f'/dataset/{name}/clone/0',
                                   follow_redirects=True)
        self.assertEqual(response.status_code, 200)
        downloaded_dataset = self._download_dataset(name)
        self.assertLen(downloaded_dataset.reactions, 81)
        self.assertEqual(dataset.reactions[0],
                         downloaded_dataset.reactions[80])

    def test_delete_reaction(self):
        name = 'test'
        dataset = self._get_dataset()
        self._upload_dataset(dataset, name)
        response = self.client.get(f'/dataset/{name}/delete/reaction/0',
                                   follow_redirects=True)
        self.assertEqual(response.status_code, 200)
        downloaded_dataset = self._download_dataset(name)
        self.assertLen(downloaded_dataset.reactions, 79)
        self.assertEqual(dataset.reactions[1], downloaded_dataset.reactions[0])

    def test_delete_reaction_id(self):
        name = 'test'
        dataset = dataset_pb2.Dataset()
        reaction_id = 'test_reaction_id'
        dataset.reaction_ids.append(reaction_id)
        self._upload_dataset(dataset, name)
        response = self.client.get(
            f'/dataset/{name}/delete/reaction_id/{reaction_id}',
            follow_redirects=True)
        self.assertEqual(response.status_code, 200)
        downloaded_dataset = self._download_dataset(name)
        self.assertEmpty(downloaded_dataset.reaction_ids)

    def test_delete_reaction_id_blank(self):
        name = 'test'
        dataset = dataset_pb2.Dataset(reaction_ids=['', 'test', ''])
        self._upload_dataset(dataset, name)
        response = self.client.get(f'/dataset/{name}/delete/reaction_id',
                                   follow_redirects=True)
        self.assertEqual(response.status_code, 200)
        downloaded_dataset = self._download_dataset(name)
        self.assertLen(downloaded_dataset.reaction_ids, 2)

    def test_read_dataset(self):
        name = 'test'
        dataset = self._get_dataset()
        self._upload_dataset(dataset, name)
        response = self.client.get(f'/dataset/proto/read/{name}',
                                   follow_redirects=True)
        self.assertEqual(response.status_code, 200)
        downloaded_dataset = dataset_pb2.Dataset()
        downloaded_dataset.ParseFromString(response.data)
        self.assertEqual(downloaded_dataset, dataset)

    def test_write_dataset(self):
        name = 'test'
        dataset = self._get_dataset()
        response = self.client.post(f'/dataset/proto/write/{name}',
                                    data=dataset.SerializeToString(),
                                    follow_redirects=True)
        self.assertEqual(response.status_code, 200)
        downloaded_dataset = self._download_dataset(name)
        self.assertEqual(downloaded_dataset, dataset)

    def test_write_upload(self):
        name = 'test'
        data = b'test data'
        token = b'upload_token'
        dataset = dataset_pb2.Dataset()
        reaction = dataset.reactions.add()
        observation = reaction.observations.add()
        observation.image.bytes_value = token
        self._upload_dataset(dataset, name)
        response = self.client.post(
            f'/dataset/proto/upload/{name}/{token.decode()}',
            data=data,
            follow_redirects=True)
        self.assertEqual(response.status_code, 200)
        # Verify that the token was resolved in the Dataset.
        downloaded_dataset = self._download_dataset(name)
        self.assertEqual(
            downloaded_dataset.reactions[0].observations[0].image.bytes_value,
            data)

    def test_read_upload(self):
        data = b'test data'
        token = 'upload_token'
        response = self.client.post(f'/dataset/proto/download/{token}',
                                    data=data,
                                    follow_redirects=True)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data, data)

    @parameterized.named_parameters([
        ('percentage', reaction_pb2.Percentage(value=15.6), 0, 0),
        ('bad_precision', reaction_pb2.Percentage(precision=-15.6), 2, 0),
    ])
    def test_validate_reaction(self, message, expected_num_errors,
                               expected_num_warnings):
        response = self.client.post(
            f'/dataset/proto/validate/{message.DESCRIPTOR.name}',
            data=message.SerializeToString(),
            follow_redirects=True)
        self.assertEqual(response.status_code, 200)
        output = json.loads(response.data)
        self.assertLen(output['errors'], expected_num_errors)
        self.assertLen(output['warnings'], expected_num_warnings)

    @parameterized.parameters([
        ('NAME', 'benzene', 'c1ccccc1'),
    ])
    def test_resolve_compound(self, identifier_type, data, expected):
        response = self.client.post(f'/resolve/{identifier_type}',
                                    data=data,
                                    follow_redirects=True)
        self.assertEqual(response.status_code, 200)
        resolved, _ = json.loads(response.data)
        # NOTE(kearnes): Try to compensate for values from different services.
        canonical_resolved = Chem.MolToSmiles(Chem.MolFromSmiles(resolved))
        self.assertEqual(canonical_resolved, expected)

    def test_render_reaction(self):
        reaction = reaction_pb2.Reaction()
        component = reaction.inputs['test'].components.add()
        component.identifiers.add(value='c1ccccc1', type='SMILES')
        response = self.client.post('/render/reaction',
                                    data=reaction.SerializeToString(),
                                    follow_redirects=True)
        self.assertEqual(response.status_code, 200)

    def test_render_compound(self):
        compound = reaction_pb2.Compound()
        compound.identifiers.add(value='c1ccccc1', type='SMILES')
        response = self.client.post('/render/reaction',
                                    data=compound.SerializeToString(),
                                    follow_redirects=True)
        self.assertEqual(response.status_code, 200)

    def test_compare(self):
        name = 'test'
        dataset = self._get_dataset()
        self._upload_dataset(dataset, name)
        response = self.client.post(f'/dataset/proto/compare/{name}',
                                    data=dataset.SerializeToString(),
                                    follow_redirects=True)
        self.assertEqual(response.status_code, 200)
        dataset.reactions[0].reaction_id = 'not the original'
        response = self.client.post(f'/dataset/proto/compare/{name}',
                                    data=dataset.SerializeToString(),
                                    follow_redirects=True)
        self.assertEqual(response.status_code, 409)

    def test_js(self):
        pass  # Requires the editor to be built.

    @parameterized.parameters([
        ('reaction.css', 200),
        ('percentage.css', 404),
    ])
    def test_css(self, sheet, expected):
        response = self.client.get(f'/css/{sheet}', follow_redirects=True)
        self.assertEqual(response.status_code, expected)

    def test_ketcher_iframe(self):
        response = self.client.get('/ketcher/iframe', follow_redirects=True)
        self.assertEqual(response.status_code, 200)

    def test_indigo(self):
        response = self.client.get('/ketcher/info', follow_redirects=True)
        self.assertEqual(response.status_code, 204)

    def test_ketcher(self):
        pass  # Ketcher is not part of the repo, so we can't test this easily.

    @parameterized.parameters([
        'dataset/deps.js',
        'dataset/test/deps.js',
        'dataset/test/reaction/deps.js',
    ])
    def test_deps(self, path):
        response = self.client.get(path, follow_redirects=True)
        self.assertEqual(response.status_code, 200)

    def test_get_molfile(self):
        smiles = 'c1ccccc1'
        compound = reaction_pb2.Compound()
        compound.identifiers.add(value=smiles, type='SMILES')
        response = self.client.post('/ketcher/molfile',
                                    data=compound.SerializeToString(),
                                    follow_redirects=True)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(json.loads(response.data),
                         Chem.MolToMolBlock(Chem.MolFromSmiles(smiles)))

    def test_get_molfile_no_structure(self):
        compound = reaction_pb2.Compound()
        compound.identifiers.add(value='benzene', type='NAME')
        response = self.client.post('/ketcher/molfile',
                                    data=compound.SerializeToString(),
                                    follow_redirects=True)
        self.assertEqual(response.status_code, 204)
Example #3
0
class ValidationsTest(parameterized.TestCase, absltest.TestCase):
    def setUp(self):
        super().setUp()
        # Redirect warning messages to stdout so they can be filtered from the
        # other test output.
        self._showwarning = warnings.showwarning

        # pylint: disable=too-many-arguments
        def _showwarning(message,
                         category,
                         filename,
                         lineno,
                         file=None,
                         line=None):
            del file  # Unused.
            self._showwarning(message=message,
                              category=category,
                              filename=filename,
                              lineno=lineno,
                              file=sys.stdout,
                              line=line)

        # pylint: enable=too-many-arguments
        warnings.showwarning = _showwarning

    def tearDown(self):
        super().tearDown()
        # Restore the original showwarning.
        warnings.showwarning = self._showwarning

    def _run_validation(self, message, **kwargs):
        original = type(message)()
        original.CopyFrom(message)
        output = validations.validate_message(message, **kwargs)
        # Verify that `message` is unchanged by the validation process.
        self.assertEqual(original, message)
        return output

    @parameterized.named_parameters(
        ('volume',
         reaction_pb2.Volume(value=15.0,
                             units=reaction_pb2.Volume.MILLILITER)),
        ('time', reaction_pb2.Time(value=24, units=reaction_pb2.Time.HOUR)),
        ('mass', reaction_pb2.Mass(value=32.1, units=reaction_pb2.Mass.GRAM)),
    )
    def test_units(self, message):
        self.assertEmpty(self._run_validation(message))

    @parameterized.named_parameters(
        ('neg volume',
         reaction_pb2.Volume(
             value=-15.0,
             units=reaction_pb2.Volume.MILLILITER), 'non-negative'),
        ('neg time', reaction_pb2.Time(
            value=-24, units=reaction_pb2.Time.HOUR), 'non-negative'),
        ('neg mass',
         reaction_pb2.Mass(value=-32.1,
                           units=reaction_pb2.Mass.GRAM), 'non-negative'),
        ('no units', reaction_pb2.FlowRate(value=5), 'units'),
        ('percentage out of range', reaction_pb2.Percentage(value=200),
         'between'),
        ('low temperature', reaction_pb2.Temperature(
            value=-5, units='KELVIN'), 'between'),
        ('low temperature 2',
         reaction_pb2.Temperature(value=-500, units='CELSIUS'), 'between'),
    )
    def test_units_should_fail(self, message, expected_error):
        with self.assertRaisesRegex(validations.ValidationError,
                                    expected_error):
            self._run_validation(message)

    def test_orcid(self):
        message = reaction_pb2.Person(orcid='0000-0001-2345-678X')
        self.assertEmpty(self._run_validation(message))

    def test_orcid_should_fail(self):
        message = reaction_pb2.Person(orcid='abcd-0001-2345-678X')
        with self.assertRaisesRegex(validations.ValidationError, 'Invalid'):
            self._run_validation(message)

    def test_reaction(self):
        message = reaction_pb2.Reaction()
        with self.assertRaisesRegex(validations.ValidationError,
                                    'reaction input'):
            self._run_validation(message)

    def test_reaction_recursive(self):
        message = reaction_pb2.Reaction()
        # Reactions must have at least one input
        with self.assertRaisesRegex(validations.ValidationError,
                                    'reaction input'):
            self._run_validation(message, recurse=False)
        dummy_input = message.inputs['dummy_input']
        # Reactions must have at least one outcome
        with self.assertRaisesRegex(validations.ValidationError,
                                    'reaction outcome'):
            self._run_validation(message, recurse=False)
        outcome = message.outcomes.add()
        self.assertEmpty(self._run_validation(message, recurse=False))
        # Inputs must have at least one component
        with self.assertRaisesRegex(validations.ValidationError, 'component'):
            self._run_validation(message)
        dummy_component = dummy_input.components.add()
        # Components must have at least one identifier
        with self.assertRaisesRegex(validations.ValidationError, 'identifier'):
            self._run_validation(message)
        dummy_component.identifiers.add(type='CUSTOM')
        # Custom identifiers must have details specified
        with self.assertRaisesRegex(validations.ValidationError, 'details'):
            self._run_validation(message)
        dummy_component.identifiers[0].details = 'custom_identifier'
        dummy_component.identifiers[0].value = 'custom_value'
        # Components of reaction inputs must have a defined amount
        with self.assertRaisesRegex(validations.ValidationError,
                                    'require an amount'):
            self._run_validation(message)
        dummy_component.mass.value = 1
        dummy_component.mass.units = reaction_pb2.Mass.GRAM
        # Reactions must have defined products or conversion
        with self.assertRaisesRegex(validations.ValidationError,
                                    'products or conversion'):
            self._run_validation(message)
        outcome.conversion.value = 75
        # If converseions are defined, must have limiting reagent flag
        with self.assertRaisesRegex(validations.ValidationError,
                                    'is_limiting'):
            self._run_validation(message)
        dummy_component.is_limiting = True
        self.assertEmpty(self._run_validation(message))

        # If an analysis uses an internal standard, a component must have
        # an INTERNAL_STANDARD reaction role
        outcome.analyses['dummy_analysis'].CopyFrom(
            reaction_pb2.ReactionAnalysis(type='CUSTOM',
                                          details='test',
                                          uses_internal_standard=True))
        with self.assertRaisesRegex(validations.ValidationError,
                                    'INTERNAL_STANDARD'):
            self._run_validation(message)
        # Assigning internal standard role to input should resolve the error
        message_input_istd = reaction_pb2.Reaction()
        message_input_istd.CopyFrom(message)
        message_input_istd.inputs['dummy_input'].components[
            0].reaction_role = (
                reaction_pb2.Compound.ReactionRole.INTERNAL_STANDARD)
        self.assertEmpty(self._run_validation(message_input_istd))
        # Assigning internal standard role to workup should resolve the error
        message_workup_istd = reaction_pb2.Reaction()
        message_workup_istd.CopyFrom(message)
        workup = message_workup_istd.workup.add(type='CUSTOM', details='test')
        istd = workup.input.components.add()
        istd.identifiers.add(type='SMILES', value='CCO')
        istd.mass.value = 1
        istd.mass.units = reaction_pb2.Mass.GRAM
        istd.reaction_role = istd.ReactionRole.INTERNAL_STANDARD
        self.assertEmpty(self._run_validation(message_workup_istd))

    def test_reaction_recursive_noraise_on_error(self):
        message = reaction_pb2.Reaction()
        message.inputs['dummy_input'].components.add()
        errors = self._run_validation(message, raise_on_error=False)
        expected = [
            'Compounds must have at least one identifier',
            "Reaction input's components require an amount",
            'Reactions should have at least 1 reaction outcome',
        ]
        self.assertEqual(errors, expected)

    def test_datetimes(self):
        message = reaction_pb2.ReactionProvenance()
        message.experiment_start.value = '11 am'
        message.record_created.time.value = '10 am'
        with self.assertRaisesRegex(validations.ValidationError, 'after'):
            self._run_validation(message)
        message.record_created.time.value = '11:15 am'
        self.assertEmpty(self._run_validation(message))

    def test_reaction_id(self):
        message = reaction_pb2.Reaction()
        _ = message.inputs['test']
        message.outcomes.add()
        message.reaction_id = 'ord-c0bbd41f095a44a78b6221135961d809'
        self.assertEmpty(self._run_validation(message, recurse=False))

    @parameterized.named_parameters(
        ('too short', 'ord-c0bbd41f095a4'),
        ('too long', 'ord-c0bbd41f095a4c0bbd41f095a4c0bbd41f095a4'),
        ('bad prefix', 'foo-c0bbd41f095a44a78b6221135961d809'),
        ('bad capitalization', 'ord-C0BBD41F095A44A78B6221135961D809'),
        ('bad characters', 'ord-h0bbd41f095a44a78b6221135961d809'),
        ('bad characters 2', 'ord-notARealId'),
    )
    def test_bad_reaction_id(self, reaction_id):
        message = reaction_pb2.Reaction(reaction_id=reaction_id)
        _ = message.inputs['test']
        message.outcomes.add()
        with self.assertRaisesRegex(validations.ValidationError, 'malformed'):
            self._run_validation(message, recurse=False)

    def test_data(self):
        message = reaction_pb2.Data()
        with self.assertRaisesRegex(validations.ValidationError,
                                    'requires one of'):
            self._run_validation(message)
        message.bytes_value = b'test data'
        with self.assertRaisesRegex(validations.ValidationError,
                                    'format is required'):
            self._run_validation(message)
        message.string_value = 'test data'
        self.assertEmpty(self._run_validation(message))

    def test_dataset_bad_reaction_id(self):
        message = dataset_pb2.Dataset(reaction_ids=['foo'])
        with self.assertRaisesRegex(validations.ValidationError, 'malformed'):
            self._run_validation(message)

    def test_dataset_records_and_ids(self):
        message = dataset_pb2.Dataset(
            reactions=[reaction_pb2.Reaction()],
            reaction_ids=['ord-c0bbd41f095a44a78b6221135961d809'])
        with self.assertRaisesRegex(validations.ValidationError, 'not both'):
            self._run_validation(message, recurse=False)

    def test_dataset_bad_id(self):
        message = dataset_pb2.Dataset(reactions=[reaction_pb2.Reaction()],
                                      dataset_id='foo')
        with self.assertRaisesRegex(validations.ValidationError, 'malformed'):
            self._run_validation(message, recurse=False)

    def test_dataset_example(self):
        message = dataset_pb2.DatasetExample()
        with self.assertRaisesRegex(validations.ValidationError,
                                    'description is required'):
            self._run_validation(message)
        message.description = 'test example'
        with self.assertRaisesRegex(validations.ValidationError,
                                    'url is required'):
            self._run_validation(message)
        message.url = 'example.com'
        with self.assertRaisesRegex(validations.ValidationError,
                                    'created is required'):
            self._run_validation(message)
        message.created.time.value = '11 am'
        self.assertEmpty(self._run_validation(message))
Example #4
0

def test_read_upload(client):
    data = b"test data"
    token = "upload_token"
    response = client.post(f"/dataset/proto/download/{token}",
                           data=data,
                           follow_redirects=True)
    assert response.status_code == 200
    assert response.data == data


@pytest.mark.parametrize(
    "message,expected_num_errors,expected_num_warnings",
    (
        (reaction_pb2.Percentage(value=15.6), 0, 0),
        (reaction_pb2.Percentage(precision=-15.6), 2, 0),
    ),
)
def test_validate_reaction(client, message, expected_num_errors,
                           expected_num_warnings):
    response = client.post(
        f"/dataset/proto/validate/{message.DESCRIPTOR.name}",
        data=message.SerializeToString(),
        follow_redirects=True)
    assert response.status_code == 200
    output = json.loads(response.data)
    assert len(output["errors"]) == expected_num_errors
    assert len(output["warnings"]) == expected_num_warnings

Example #5
0
class ValidationsTest(parameterized.TestCase, absltest.TestCase):
    @parameterized.named_parameters(
        ('volume',
         reaction_pb2.Volume(value=15.0,
                             units=reaction_pb2.Volume.MILLILITER)),
        ('time', reaction_pb2.Time(value=24, units=reaction_pb2.Time.HOUR)),
        ('mass', reaction_pb2.Mass(value=32.1, units=reaction_pb2.Mass.GRAM)),
    )
    def test_units(self, message):
        self.assertEmpty(validations.validate_message(message))

    @parameterized.named_parameters(
        ('neg volume',
         reaction_pb2.Volume(
             value=-15.0,
             units=reaction_pb2.Volume.MILLILITER), 'non-negative'),
        ('neg time', reaction_pb2.Time(
            value=-24, units=reaction_pb2.Time.HOUR), 'non-negative'),
        ('neg mass',
         reaction_pb2.Mass(value=-32.1,
                           units=reaction_pb2.Mass.GRAM), 'non-negative'),
        ('no units', reaction_pb2.FlowRate(value=5), 'units'),
        ('percentage out of range', reaction_pb2.Percentage(value=200),
         'between'),
        ('low temperature', reaction_pb2.Temperature(
            value=-5, units='KELVIN'), 'between'),
        ('low temperature 2',
         reaction_pb2.Temperature(value=-500, units='CELSIUS'), 'between'),
    )
    def test_units_should_fail(self, message, expected_error):
        with self.assertRaisesRegex(validations.ValidationError,
                                    expected_error):
            validations.validate_message(message)

    def test_orcid(self):
        message = reaction_pb2.Person(orcid='0000-0001-2345-678X')
        self.assertEmpty(validations.validate_message(message))

    def test_orcid_should_fail(self):
        message = reaction_pb2.Person(orcid='abcd-0001-2345-678X')
        with self.assertRaisesRegex(validations.ValidationError, 'Invalid'):
            validations.validate_message(message)

    def test_reaction(self):
        message = reaction_pb2.Reaction()
        with self.assertRaisesRegex(validations.ValidationError,
                                    'reaction input'):
            validations.validate_message(message)

    def test_reaction_recursive(self):
        message = reaction_pb2.Reaction()
        # Reactions must have at least one input
        with self.assertRaisesRegex(validations.ValidationError,
                                    'reaction input'):
            validations.validate_message(message, recurse=False)
        dummy_input = message.inputs['dummy_input']
        # Reactions must have at least one outcome
        with self.assertRaisesRegex(validations.ValidationError,
                                    'reaction outcome'):
            validations.validate_message(message, recurse=False)
        outcome = message.outcomes.add()
        self.assertEmpty(validations.validate_message(message, recurse=False))
        # Inputs must have at least one component
        with self.assertRaisesRegex(validations.ValidationError, 'component'):
            validations.validate_message(message)
        dummy_component = dummy_input.components.add()
        # Components must have at least one identifier
        with self.assertRaisesRegex(validations.ValidationError, 'identifier'):
            validations.validate_message(message)
        dummy_component.identifiers.add(type='CUSTOM')
        # Custom identifiers must have details specified
        with self.assertRaisesRegex(validations.ValidationError, 'details'):
            validations.validate_message(message)
        dummy_component.identifiers[0].details = 'custom_identifier'
        dummy_component.identifiers[0].value = 'custom_value'
        # Components of reaction inputs must have a defined amount
        with self.assertRaisesRegex(validations.ValidationError,
                                    'require an amount'):
            validations.validate_message(message)
        dummy_component.mass.value = 1
        dummy_component.mass.units = reaction_pb2.Mass.GRAM
        # Reactions must have defined products or conversion
        with self.assertRaisesRegex(validations.ValidationError,
                                    'products or conversion'):
            validations.validate_message(message)
        outcome.conversion.value = 75
        # If converseions are defined, must have limiting reagent flag
        with self.assertRaisesRegex(validations.ValidationError,
                                    'is_limiting'):
            validations.validate_message(message)
        dummy_component.is_limiting = True
        self.assertEmpty(validations.validate_message(message))

        # If an analysis uses an internal standard, a component must have
        # an INTERNAL_STANDARD reaction role
        outcome.analyses['dummy_analysis'].uses_internal_standard = True
        with self.assertRaisesRegex(validations.ValidationError,
                                    'INTERNAL_STANDARD'):
            validations.validate_message(message)
        # Assigning internal standard role to input should resolve the error
        message_input_istd = reaction_pb2.Reaction()
        message_input_istd.CopyFrom(message)
        message_input_istd.inputs['dummy_input'].components[
            0].reaction_role = (
                reaction_pb2.Compound.ReactionRole.INTERNAL_STANDARD)
        self.assertEmpty(validations.validate_message(message_input_istd))
        # Assigning internal standard role to workup should resolve the error
        message_workup_istd = reaction_pb2.Reaction()
        message_workup_istd.CopyFrom(message)
        workup = message_workup_istd.workup.add()
        istd = workup.components.add()
        istd.identifiers.add(type='SMILES', value='CCO')
        istd.mass.value = 1
        istd.mass.units = reaction_pb2.Mass.GRAM
        istd.reaction_role = istd.ReactionRole.INTERNAL_STANDARD
        self.assertEmpty(validations.validate_message(message_workup_istd))

    def test_reaction_recursive_noraise_on_error(self):
        message = reaction_pb2.Reaction()
        message.inputs['dummy_input'].components.add()
        errors = validations.validate_message(message, raise_on_error=False)
        expected = [
            'Compounds must have at least one identifier',
            "Reaction input's components require an amount",
            'Reactions should have at least 1 reaction outcome',
        ]
        self.assertEqual(errors, expected)

    def test_datetimes(self):
        message = reaction_pb2.ReactionProvenance()
        message.experiment_start.value = '11 am'
        message.record_created.time.value = '10 am'
        with self.assertRaisesRegex(validations.ValidationError, 'after'):
            validations.validate_message(message)
        message.record_created.time.value = '11:15 am'
        self.assertEmpty(validations.validate_message(message))

    def test_record_id(self):
        message = reaction_pb2.ReactionProvenance()
        message.record_created.time.value = '10 am'
        message.record_id = 'ord-c0bbd41f095a44a78b6221135961d809'
        self.assertEmpty(validations.validate_message(message))

    @parameterized.named_parameters(
        ('too short', 'ord-c0bbd41f095a4'),
        ('too long', 'ord-c0bbd41f095a4c0bbd41f095a4c0bbd41f095a4'),
        ('bad prefix', 'foo-c0bbd41f095a44a78b6221135961d809'),
        ('bad capitalization', 'ord-C0BBD41F095A44A78B6221135961D809'),
        ('bad characters', 'ord-h0bbd41f095a44a78b6221135961d809'),
        ('bad characters 2', 'ord-notARealId'),
    )
    def test_bad_record_id(self, record_id):
        message = reaction_pb2.ReactionProvenance()
        message.record_created.time.value = '10 am'
        message.record_id = record_id
        with self.assertRaisesRegex(validations.ValidationError, 'malformed'):
            validations.validate_message(message)

    def test_compound_name_resolver(self):
        message = reaction_pb2.Compound()
        identifier = message.identifiers.add()
        identifier.type = identifier.NAME
        identifier.value = 'aspirin'
        validations.validate_message(message)  # Message is modified in place.
        self.assertEqual(
            message.identifiers[1],
            reaction_pb2.CompoundIdentifier(
                type='SMILES',
                value='CC(=O)OC1=CC=CC=C1C(=O)O',
                details='NAME resolved by PubChem'))

    @absltest.skipIf(Chem is None, 'no rdkit')
    def test_compound_rdkit_binary(self):
        mol = Chem.MolFromSmiles('CC(=O)OC1=CC=CC=C1C(=O)O')
        message = reaction_pb2.Compound()
        identifier = message.identifiers.add()
        identifier.type = identifier.SMILES
        identifier.value = Chem.MolToSmiles(mol)
        validations.validate_message(message)  # Message is modified in place.
        self.assertEqual(
            message.identifiers[1],
            reaction_pb2.CompoundIdentifier(type='RDKIT_BINARY',
                                            bytes_value=mol.ToBinary()))

    def test_data(self):
        message = reaction_pb2.Data()
        with self.assertRaisesRegex(validations.ValidationError,
                                    'requires one of'):
            validations.validate_message(message)
        message.bytes_value = b'test data'
        with self.assertRaisesRegex(validations.ValidationError,
                                    'format is required'):
            validations.validate_message(message)
        message.value = 'test data'
        self.assertEmpty(validations.validate_message(message))