def test_object_pointer_serde(): """Test that an object can point to another object, and that this survives serde.""" baking = ProcessRun("Bake a cake") cake = MaterialRun("A cake", process=baking) reconstituted_cake = MaterialRun.build(cake.dump()) assert isinstance(reconstituted_cake.process, ProcessRun) assert isinstance(reconstituted_cake.process.output_material, MaterialRun)
def test_build_discarded_objects_in_material_run(): # Note: This is really here just for test coverage - couldn't figure out how to # get past the validation/serialization in MaterialRun.build - it might just be dead code material_run = MaterialRunFactory() material_run_data = MaterialRunDataFactory( name='Test Run', measurements=LinkByUIDFactory.create_batch(3)) MaterialRun._build_discarded_objects(material_run, material_run_data, None)
def test_process_attachment(): """Test that a process can be attached to a material, and that the connection survives serde""" cake = MaterialRun('Final cake') cake.process = ProcessRun('Icing') cake_data = cake.dump() cake_copy = loads(dumps(cake_data)).as_dict() assert cake_copy['name'] == cake.name assert cake_copy['uids'] == cake.uids assert cake.process.uids['id'] == cake_copy['process'].uids['id'] reconstituted_cake = MaterialRun.build(cake_copy) assert isinstance(reconstituted_cake, MaterialRun) assert isinstance(reconstituted_cake.process, ProcessRun)
def test_simple_deserialization(valid_data): """Ensure that a deserialized Measurement Run looks sane.""" measurement_run: MeasurementRun = MeasurementRun.build(valid_data) assert measurement_run.uids == {'id': valid_data['uids']['id']} assert measurement_run.name == 'Taste test' assert measurement_run.notes is None assert measurement_run.tags == [] assert measurement_run.conditions == [] assert measurement_run.parameters == [] assert measurement_run.properties[0] == Property('sweetness', origin="measured", value=NominalInteger(7)) assert measurement_run.properties[1] == Property('fluffiness', origin="measured", value=NominalInteger(10)) assert measurement_run.file_links == [] assert measurement_run.template is None assert measurement_run.material == MaterialRun( 'sponge cake', uids={'id': valid_data['material']['uids']['id']}, sample_type='experimental') assert measurement_run.material.audit_info == AuditInfo( **valid_data['material']['audit_info']) assert measurement_run.material.dataset == UUID( valid_data['material']['dataset']) assert measurement_run.spec is None assert measurement_run.typ == 'measurement_run' assert measurement_run.audit_info == AuditInfo(**valid_data['audit_info']) assert measurement_run.dataset == UUID(valid_data['dataset'])
def test_material_attachment(): """Test that a material can be attached to a measurement, and the connection survives serde.""" cake = MaterialRun('Final Cake') mass = MeasurementRun('Weigh cake', material=cake) mass_data = mass.dump() mass_copy = MeasurementRun.build(mass_data) assert mass_copy == mass
def test_default_for_material(collection: AraDefinitionCollection, session): """Test that default for material hits the right route""" # Given project_id = '6b608f78-e341-422c-8076-35adc8828545' dummy_resp = { 'config': TableConfig( name='foo', description='foo', variables=[], columns=[], rows=[], datasets=[] ).dump(), 'ambiguous': [ [ RootIdentifier(name='foo', headers=['foo'], scope='id').dump(), IdentityColumn(data_source='foo').dump(), ] ], } session.responses.append(dummy_resp) collection.default_for_material( material='my_id', scope='my_scope', name='my_name', description='my_description', ) assert 1 == session.num_calls assert session.last_call == FakeCall( method="GET", path="projects/{}/table-configs/default".format(project_id), params={ 'id': 'my_id', 'scope': 'my_scope', 'name': 'my_name', 'description': 'my_description' } ) session.calls.clear() session.responses.append(dummy_resp) collection.default_for_material( material=MaterialRun('foo', uids={'scope': 'id'}), scope='ignored', name='my_name', description='my_description', ) assert 1 == session.num_calls assert session.last_call == FakeCall( method="GET", path="projects/{}/table-configs/default".format(project_id), params={ 'id': 'id', 'scope': 'scope', 'name': 'my_name', 'description': 'my_description' } )
def test_material_attachment(): """ Attach a material run to an ingredient run. Check that the ingredient can be built, and that the connection survives ser/de. """ flour = MaterialRun("flour", sample_type='unknown') flour_ingredient = IngredientRun(material=flour, absolute_quantity=NominalReal(500, 'g')) flour_ingredient_copy = IngredientRun.build(flour_ingredient.dump()) assert flour_ingredient_copy == flour_ingredient
def test_soft_measurement_material_attachment(): """Test that soft attachments are formed from materials to measurements.""" cake = MaterialRun("A cake") smell_test = MeasurementRun("use your nose", material=cake, properties=[ Property( name="Smell", value=DiscreteCategorical("yummy")) ]) taste_test = MeasurementRun("taste", material=cake) assert cake.measurements == [smell_test, taste_test]
def test_delete_data_concepts(dataset): """Check that delete routes to the correct collections""" expected = { MaterialTemplateCollection: MaterialTemplate("foo", uids={"foo": "bar"}), MaterialSpecCollection: MaterialSpec("foo", uids={"foo": "bar"}), MaterialRunCollection: MaterialRun("foo", uids={"foo": "bar"}), ProcessTemplateCollection: ProcessTemplate("foo", uids={"foo": "bar"}), } for collection, obj in expected.items(): dataset.delete(obj) assert dataset.session.calls[-1].path.split("/")[-3] == basename( collection._path_template)
def test_simple_deserialization(): """Ensure that a deserialized Material Run looks sane.""" valid_data: dict = MaterialRunDataFactory(name='Cake 1') material_run: MaterialRun = MaterialRun.build(valid_data) assert material_run.uids == {'id': valid_data['uids']['id']} assert material_run.name == 'Cake 1' assert material_run.tags == ["color"] assert material_run.notes is None assert material_run.process == LinkByUID('id', valid_data['process']['id']) assert material_run.sample_type == 'experimental' assert material_run.template is None assert material_run.spec is None assert material_run.file_links == [] assert material_run.typ == 'material_run'
def test_measurement_material_connection_rehydration(): """Test that fully-linked GEMD object can be built as fully-linked Citrine-python object.""" starting_mat_spec = GEMDMaterialSpec("starting material") starting_mat = GEMDMaterialRun("starting material", spec=starting_mat_spec) meas_spec = GEMDMeasurementSpec("measurement spec") meas1 = GEMDMeasurementRun("measurement on starting material", spec=meas_spec, material=starting_mat) process_spec = GEMDProcessSpec("Transformative process") process = GEMDProcessRun("Transformative process", spec=process_spec) ingredient_spec = GEMDIngredientSpec(name="ingredient", material=starting_mat_spec, process=process_spec) ingredient = GEMDIngredientRun(material=starting_mat, process=process, spec=ingredient_spec) ending_mat_spec = GEMDMaterialSpec("ending material", process=process_spec) ending_mat = GEMDMaterialRun("ending material", process=process, spec=ending_mat_spec) meas2 = GEMDMeasurementRun("measurement on ending material", spec=meas_spec, material=ending_mat) copy = MaterialRun.build(ending_mat) assert isinstance(copy, MaterialRun), "copy of ending_mat should be a MaterialRun" assert len(copy.measurements) == 1, "copy of ending_mat should have one measurement" assert isinstance(copy.measurements[0], MeasurementRun), \ "copy of ending_mat should have a measurement that is a MeasurementRun" assert isinstance(copy.measurements[0].spec, MeasurementSpec), \ "copy of ending_mat should have a measurement that has a spec that is a MeasurementSpec" assert isinstance(copy.process, ProcessRun), "copy of ending_mat should have a process" assert len(copy.process.ingredients) == 1, \ "copy of ending_mat should have a process with one ingredient" assert isinstance(copy.spec, MaterialSpec), "copy of ending_mat should have a spec" assert len(copy.spec.process.ingredients) == 1, \ "copy of ending_mat should have a spec with a process that has one ingredient" assert isinstance(copy.process.spec.ingredients[0], IngredientSpec), \ "copy of ending_mat should have a spec with a process that has an ingredient " \ "that is an IngredientRun" copy_ingredient = copy.process.ingredients[0] assert isinstance(copy_ingredient, IngredientRun), \ "copy of ending_mat should have a process with an ingredient that is an IngredientRun" assert isinstance(copy_ingredient.material, MaterialRun), \ "copy of ending_mat should have a process with an ingredient that links to a MaterialRun" assert len(copy_ingredient.material.measurements) == 1, \ "copy of ending_mat should have a process with an ingredient derived from a material " \ "that has one measurement performed on it" assert isinstance(copy_ingredient.material.measurements[0], MeasurementRun), \ "copy of ending_mat should have a process with an ingredient derived from a material " \ "that has one measurement that gets deserialized as a MeasurementRun" assert isinstance(copy_ingredient.material.measurements[0].spec, MeasurementSpec), \ "copy of ending_mat should have a process with an ingredient derived from a material " \ "that has one measurement that has a spec"
def test_cake(): """Test that the cake example from gemd can be built without modification. This only tests the fix to a limited problem (not all ingredients being reconstructed) and is not a full test of equivalence, because the reconstruction creates "dangling paths." Consider a material/process run/spec square. The material run links to a material spec, which links to a process spec. The material run also links to a process run that links to a process spec, but it's a different process spec, and is not linked to the material spec. If you try to call mat.process.spec.output_material it returns None. This is due to the way the build() method attempts to traverse the object tree, and requires an overhaul of build(). """ gemd_cake = make_cake() cake = MaterialRun.build(gemd_cake) assert [ingred.name for ingred in cake.process.ingredients] == \ [ingred.name for ingred in gemd_cake.process.ingredients]
def test_list_validation(): """Test that lists are validated by taurus.""" mat = MaterialRun("A material") with pytest.raises(TypeError): # labels must be a list of string, but contains an int IngredientRun(material=mat, labels=["Label 1", 17], name="foo") ingredient = IngredientRun(material=mat, labels=["Label 1", "label 2"], name="foo") with pytest.raises(TypeError): # cannot append an int to a list of strings ingredient.labels.append(17) with pytest.raises(TypeError): # list of conditions cannot contain a property MeasurementRun("A measurement", conditions=[Property("not a condition")])
def test_register_data_concepts(dataset): """Check that register routes to the correct collections""" expected = { MaterialTemplateCollection: MaterialTemplate("foo"), MaterialSpecCollection: MaterialSpec("foo"), MaterialRunCollection: MaterialRun("foo"), ProcessTemplateCollection: ProcessTemplate("foo"), ProcessSpecCollection: ProcessSpec("foo"), ProcessRunCollection: ProcessRun("foo"), MeasurementTemplateCollection: MeasurementTemplate("foo"), MeasurementSpecCollection: MeasurementSpec("foo"), MeasurementRunCollection: MeasurementRun("foo"), IngredientSpecCollection: IngredientSpec("foo"), IngredientRunCollection: IngredientRun(), PropertyTemplateCollection: PropertyTemplate("bar", bounds=IntegerBounds(0, 1)), ParameterTemplateCollection: ParameterTemplate("bar", bounds=IntegerBounds(0, 1)), ConditionTemplateCollection: ConditionTemplate("bar", bounds=IntegerBounds(0, 1)) } for collection, obj in expected.items(): assert len(obj.uids) == 0 registered = dataset.register(obj) assert len(obj.uids) == 1 assert len(registered.uids) == 1 assert basename(dataset.session.calls[-1].path) == basename( collection._path_template) for pair in obj.uids.items(): assert pair[1] == registered.uids[pair[0]]
def test_default_for_material_failure(collection: AraDefinitionCollection): with pytest.raises(ValueError): collection.default_for_material( material=MaterialRun('foo'), name='foo' )
def test_serialization(): """Ensure that a serialized Material Run looks sane.""" valid_data: dict = MaterialRunDataFactory() material_run: MaterialRun = MaterialRun.build(valid_data) serialized = material_run.dump() assert serialized == valid_data
def test_nested_serialization(): """Create a bunch of nested objects and make sure that nothing breaks.""" # helper def make_ingredient(material: MaterialRun): return IngredientRun(name=material.name, material=material) icing = ProcessRun(name="Icing") cake = MaterialRun(name='Final cake', process=icing) cake.process.ingredients.append(make_ingredient(MaterialRun('Baked Cake'))) cake.process.ingredients.append(make_ingredient(MaterialRun('Frosting'))) baked = cake.process.ingredients[0].material baked.process = ProcessRun(name='Baking') baked.process.ingredients.append(make_ingredient(MaterialRun('Batter'))) batter = baked.process.ingredients[0].material batter.process = ProcessRun(name='Mixing batter') batter.process.ingredients.append( make_ingredient(material=MaterialRun('Butter'))) batter.process.ingredients.append( make_ingredient(material=MaterialRun('Sugar'))) batter.process.ingredients.append( make_ingredient(material=MaterialRun('Flour'))) batter.process.ingredients.append( make_ingredient(material=MaterialRun('Milk'))) cake.dump()
def test_register_all_data_concepts(dataset): """Check that register_all registers everything and routes to all collections""" bounds = IntegerBounds(0, 1) property_template = PropertyTemplate("bar", bounds=bounds) parameter_template = ParameterTemplate("bar", bounds=bounds) condition_template = ConditionTemplate("bar", bounds=bounds) foo_process_template = ProcessTemplate( "foo", conditions=[[condition_template, bounds]], parameters=[[parameter_template, bounds]]) foo_process_spec = ProcessSpec("foo", template=foo_process_template) foo_process_run = ProcessRun("foo", spec=foo_process_spec) foo_material_template = MaterialTemplate( "foo", properties=[[property_template, bounds]]) foo_material_spec = MaterialSpec("foo", template=foo_material_template, process=foo_process_spec) foo_material_run = MaterialRun("foo", spec=foo_material_spec, process=foo_process_run) baz_template = MaterialTemplate("baz") foo_measurement_template = MeasurementTemplate( "foo", conditions=[[condition_template, bounds]], parameters=[[parameter_template, bounds]], properties=[[property_template, bounds]]) foo_measurement_spec = MeasurementSpec("foo", template=foo_measurement_template) foo_measurement_run = MeasurementRun("foo", spec=foo_measurement_spec, material=foo_material_run) foo_ingredient_spec = IngredientSpec("foo", material=foo_material_spec, process=foo_process_spec) foo_ingredient_run = IngredientRun(spec=foo_ingredient_spec, material=foo_material_run, process=foo_process_run) baz_run = MeasurementRun("baz") # worst order possible expected = { foo_ingredient_run: IngredientRunCollection, foo_ingredient_spec: IngredientSpecCollection, foo_measurement_run: MeasurementRunCollection, foo_measurement_spec: MeasurementSpecCollection, foo_measurement_template: MeasurementTemplateCollection, foo_material_run: MaterialRunCollection, foo_material_spec: MaterialSpecCollection, foo_material_template: MaterialTemplateCollection, foo_process_run: ProcessRunCollection, foo_process_spec: ProcessSpecCollection, foo_process_template: ProcessTemplateCollection, baz_template: MaterialTemplateCollection, baz_run: MeasurementRunCollection, property_template: PropertyTemplateCollection, parameter_template: ParameterTemplateCollection, condition_template: ConditionTemplateCollection } for obj in expected: assert len(obj.uids) == 0 # All should be without ids registered = dataset.register_all(expected.keys()) assert len(registered) == len(expected) seen_ids = set() for obj in expected: assert len(obj.uids) == 1 # All should now have exactly 1 id for pair in obj.uids.items(): assert pair not in seen_ids # All ids are different seen_ids.add(pair) for obj in registered: for pair in obj.uids.items(): assert pair in seen_ids # registered items have the same ids call_basenames = [ call.path.split('/')[-2] for call in dataset.session.calls ] collection_basenames = [ basename(collection._path_template) for collection in expected.values() ] assert set(call_basenames) == set(collection_basenames) assert len(set(call_basenames)) == len( call_basenames) # calls are batched internally # spot check order. Does not check every constraint assert call_basenames.index( basename( IngredientRunCollection._path_template)) > call_basenames.index( basename(IngredientSpecCollection._path_template)) assert call_basenames.index(basename( MaterialRunCollection._path_template)) > call_basenames.index( basename(MaterialSpecCollection._path_template)) assert call_basenames.index( basename( MeasurementRunCollection._path_template)) > call_basenames.index( basename(MeasurementSpecCollection._path_template)) assert call_basenames.index(basename( ProcessRunCollection._path_template)) > call_basenames.index( basename(ProcessSpecCollection._path_template)) assert call_basenames.index(basename( MaterialSpecCollection._path_template)) > call_basenames.index( basename(MaterialTemplateCollection._path_template)) assert call_basenames.index( basename( MeasurementSpecCollection._path_template)) > call_basenames.index( basename(MeasurementTemplateCollection._path_template)) assert call_basenames.index(basename( ProcessSpecCollection._path_template)) > call_basenames.index( basename(ProcessTemplateCollection._path_template)) assert call_basenames.index(basename( MaterialSpecCollection._path_template)) > call_basenames.index( basename(ProcessSpecCollection._path_template)) assert call_basenames.index(basename( MaterialSpecCollection._path_template)) > call_basenames.index( basename(MeasurementSpecCollection._path_template)) assert call_basenames.index( basename(MeasurementTemplateCollection._path_template) ) > call_basenames.index( basename(ConditionTemplateCollection._path_template)) assert call_basenames.index( basename(MeasurementTemplateCollection._path_template) ) > call_basenames.index( basename(ParameterTemplateCollection._path_template)) assert call_basenames.index( basename( MaterialTemplateCollection._path_template)) > call_basenames.index( basename(PropertyTemplateCollection._path_template))
def test_default_for_material(collection: TableConfigCollection, session): """Test that default for material hits the right route""" # Given project_id = '6b608f78-e341-422c-8076-35adc8828545' dummy_resp = { 'config': TableConfig(name='foo', description='foo', variables=[], columns=[], rows=[], datasets=[]).dump(), 'ambiguous': [[ RootIdentifier(name='foo', headers=['foo'], scope='id').dump(), IdentityColumn(data_source='foo').dump(), ]], } # Specify by Citrine ID session.responses.append(dummy_resp) collection.default_for_material( material='my_id', name='my_name', description='my_description', ) assert 1 == session.num_calls assert session.last_call == FakeCall( method="GET", path="projects/{}/table-configs/default".format(project_id), params={ 'id': 'my_id', 'scope': CITRINE_SCOPE, 'name': 'my_name', 'description': 'my_description' }) # Specify by id with custom scope, throwing a warning session.calls.clear() session.responses.append(dummy_resp) with warnings.catch_warnings(record=True) as caught: collection.default_for_material(material='my_id', scope='my_scope', name='my_name', description='my_description') assert len(caught) == 1 assert issubclass(caught[0].category, DeprecationWarning) assert 1 == session.num_calls assert session.last_call == FakeCall( method="GET", path="projects/{}/table-configs/default".format(project_id), params={ 'id': 'my_id', 'scope': 'my_scope', 'name': 'my_name', 'description': 'my_description' }) # Specify by MaterialRun session.calls.clear() session.responses.append(dummy_resp) collection.default_for_material(material=MaterialRun('foo', uids={'scope': 'id'}), name='my_name', description='my_description', algorithm=TableBuildAlgorithm.FORMULATIONS) assert 1 == session.num_calls assert session.last_call == FakeCall( method="GET", path="projects/{}/table-configs/default".format(project_id), params={ 'id': 'id', 'scope': 'scope', 'name': 'my_name', 'description': 'my_description', 'algorithm': TableBuildAlgorithm.FORMULATIONS.value }) # Specify by LinkByUID session.calls.clear() session.responses.append(dummy_resp) collection.default_for_material( material=LinkByUID(scope="scope", id="id"), name='my_name', description='my_description', ) assert 1 == session.num_calls assert session.last_call == FakeCall( method="GET", path="projects/{}/table-configs/default".format(project_id), params={ 'id': 'id', 'scope': 'scope', 'name': 'my_name', 'description': 'my_description' }) # And we allowed for the more forgiving call structure, so test it. session.calls.clear() session.responses.append(dummy_resp) collection.default_for_material( material=MaterialRun('foo', uids={'scope': 'id'}), scope='ignored', algorithm=TableBuildAlgorithm.FORMULATIONS.value, name='my_name', description='my_description', ) assert 1 == session.num_calls assert session.last_call == FakeCall( method="GET", path="projects/{}/table-configs/default".format(project_id), params={ 'id': 'id', 'scope': 'scope', 'algorithm': TableBuildAlgorithm.FORMULATIONS.value, 'name': 'my_name', 'description': 'my_description' })
def test_soft_process_material_attachment(): """Test that soft attachments are formed from process to output material""" baking = ProcessRun("Bake a cake") cake = MaterialRun("A cake", process=baking) assert baking.output_material == cake
def test_object_validation(): """Test that an object pointing to another object is validated.""" meas = MeasurementRun("A measurement") with pytest.raises(TypeError): MaterialRun("A cake", process=meas)