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
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)
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))
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
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))