def test_material_run(): """ Test the ability to create a MaterialRun that is linked to a MaterialSpec. Make sure all enumerated values are respected, and check consistency after serializing and deserializing. """ # Define a property, and make sure that an inappropriate value for origin throws ValueError with pytest.raises(ValueError): prop = Property(name="A property", origin="bad origin", value=NominalReal(17, units='')) # Create a MaterialSpec with a property prop = Property(name="A property", origin="specified", value=NominalReal(17, units='')) mat_spec = MaterialSpec( name="a specification for a material", properties=PropertyAndConditions(prop), notes="Funny lookin'" ) # Make sure that when property is serialized, origin (an enumeration) is serialized as a string copy_prop = json.loads(dumps(mat_spec)) copy_origin = copy_prop["context"][0]["properties"][0]['property']['origin'] assert isinstance(copy_origin, str) # Create a MaterialRun, and make sure an inappropriate value for sample_type throws ValueError with pytest.raises(ValueError): mat = MaterialRun("name", spec=mat_spec, sample_type="imaginary") mat = MaterialRun("name", spec=mat_spec, sample_type="virtual") # ensure that serialization does not change the MaterialRun copy = loads(dumps(mat)) assert dumps(copy) == dumps(mat), \ "Material run is modified by serialization or deserialization"
def test_serialize(): """Serializing a nested object should be identical to individually serializing each piece.""" condition = Condition(name="A condition", value=NominalReal(7, '')) parameter = Parameter(name="A parameter", value=NormalReal(mean=17, std=1, units='')) input_material = MaterialRun(tags="input") process = ProcessRun(tags="A tag on a process run") ingredient = IngredientRun(material=input_material, process=process) material = MaterialRun(tags=["A tag on a material"], process=process) measurement = MeasurementRun(tags="A tag on a measurement", conditions=condition, parameters=parameter, material=material) # serialize the root of the tree native_object = json.loads(dumps(measurement)) # ingredients don't get serialized on the process assert (len(native_object["context"]) == 5) assert (native_object["object"]["type"] == LinkByUID.typ) # serialize all of the nodes native_batch = json.loads( dumps([material, process, measurement, ingredient])) assert (len(native_batch["context"]) == 5) assert (len(native_batch["object"]) == 4) assert (all(x["type"] == LinkByUID.typ for x in native_batch["object"]))
def test_complex_substitutions(): """Make sure accounting works for realistic objects.""" root = MaterialRun("root", process=ProcessRun("root", spec=ProcessSpec("root")), spec=MaterialSpec("root")) root.spec.process = root.process.spec input = MaterialRun("input", process=ProcessRun("input", spec=ProcessSpec("input")), spec=MaterialSpec("input")) input.spec.process = input.process.spec IngredientRun(process=root.process, material=input, spec=IngredientSpec("ingredient", process=root.process.spec, material=input.spec)) param = ParameterTemplate("Param", bounds=RealBounds(-1, 1, "m")) root.process.spec.template = ProcessTemplate("Proc", parameters=[param]) root.process.parameters.append( Parameter("Param", value=NormalReal(0, 1, 'm'), template=param)) links = flatten(root, scope="test-scope") index = make_index(links) rebuild = substitute_objects(links, index, inplace=True) rebuilt_root = next(x for x in rebuild if x.name == root.name and x.typ == root.typ) all_objs = recursive_flatmap(rebuilt_root, func=lambda x: [x], unidirectional=False) unique = [x for i, x in enumerate(all_objs) if i == all_objs.index(x)] assert not any(isinstance(x, LinkByUID) for x in unique), "All are objects" assert len(links) == len(unique), "Objects are missing"
def test_invalid_assignment(): """Invalid assignments to `process` or `spec` throw a TypeError.""" with pytest.raises(TypeError): MaterialRun(name=12) with pytest.raises(TypeError): MaterialRun("name", spec=ProcessRun("a process")) with pytest.raises(TypeError): MaterialRun("name", process=MaterialSpec("a spec"))
def test_equality(): """Test that equality check works as expected.""" spec = MaterialSpec( "A spec", properties=PropertyAndConditions( property=Property("a property", value=NominalReal(3, ''))), tags=["a tag"]) mat1 = MaterialRun("A material", spec=spec) mat2 = MaterialRun("A material", spec=spec, tags=["A tag"]) assert mat1 == deepcopy(mat1) assert mat1 != mat2 assert mat1 != "A material"
def test_exceptions(): """Additional tests to get full coverage on exceptions.""" with pytest.raises(ValueError): add_edge(MaterialRun("Input"), make_node("Output")) with pytest.raises(ValueError): add_edge(make_node("Input"), MaterialRun("Output", spec=LinkByUID("Bad", "ID"))) with pytest.raises(ValueError): add_measurement(make_node('Material'), name='Measurement', attributes=[UnsupportedAttribute("Spider-man")]) with pytest.raises(ValueError): make_attribute(UnsupportedAttributeTemplate, 5)
def test_process_id_link(): """Test that a process run can house a LinkByUID object, and that it survives serde.""" uid = str(uuid4()) proc_link = LinkByUID(scope='id', id=uid) mat_run = MaterialRun("Another cake", process=proc_link) copy_material = loads(dumps(mat_run)) assert dumps(copy_material) == dumps(mat_run)
def test_dict_serialization(): """Test that a dictionary can be serialized and then deserialized as a gemd object.""" process = ProcessRun("A process") mat = MaterialRun("A material", process=process) meas = MeasurementRun("A measurement", material=mat) copy = loads(dumps(meas.as_dict())) assert copy == meas
def test_order_objects(): """Test that sorting works when given objects.""" unsorted = [MaterialRun("bar"), ProcessRun("foo")] sorted_list = sorted(unsorted, key=lambda x: writable_sort_order(x)) assert isinstance(sorted_list[0], ProcessRun) assert isinstance(sorted_list[1], MaterialRun)
def test_uid_deser(): """Test that uids continue to be a CaseInsensitiveDict after deserialization.""" material = MaterialRun("Input material", tags="input", uids={'Sample ID': '500-B'}) ingredient = IngredientRun(material=material) ingredient_copy = loads(dumps(ingredient)) assert isinstance(ingredient_copy.uids, CaseInsensitiveDict) assert ingredient_copy.material == material assert ingredient_copy.material.uids['sample id'] == material.uids['Sample ID']
def test_measurement_reassignment(): """Check that a measurement run can be re-assigned to a new material run.""" sample1 = MaterialRun("Sample 1") sample2 = MaterialRun("Sample 2") mass = MeasurementRun("Mass of sample", material=sample1) volume = MeasurementRun("Volume of sample", material=sample1) assert mass.material == sample1 assert set(sample1.measurements) == {mass, volume} assert sample2.measurements == [] mass.material = sample2 assert mass.material == sample2 assert sample1.measurements == [volume] assert sample2.measurements == [mass] mass.material = None assert mass.material is None assert sample2.measurements == []
def test_template_access(): """A material run's template should be equal to its spec's template.""" template = MaterialTemplate("material template", uids={'id': str(uuid4())}) spec = MaterialSpec("A spec", uids={'id': str(uuid4())}, template=template) mat = MaterialRun("A run", uids=['id', str(uuid4())], spec=spec) assert mat.template == template mat.spec = LinkByUID.from_entity(spec) assert mat.template is None
def test_build(): """Test that build recreates the material.""" spec = MaterialSpec("A spec", properties=PropertyAndConditions( property=Property("a property", value=NominalReal(3, ''))), tags=["a tag"]) mat = MaterialRun(name="a material", spec=spec) mat_dict = mat.as_dict() mat_dict['spec'] = mat.spec.as_dict() assert MaterialRun.build(mat_dict) == mat
def test_dependencies(): """Test that dependency lists make sense.""" ps = ProcessSpec(name="ps") pr = ProcessRun(name="pr", spec=ps) ms = MaterialSpec(name="ms", process=ps) mr = MaterialRun(name="mr", spec=ms, process=pr) assert ps not in mr.all_dependencies() assert pr in mr.all_dependencies() assert ms in mr.all_dependencies()
def test_make_index(): """Test functionality of make_index method.""" ps1 = ProcessSpec(name="hello", uids={"test_scope": "test_value"}) pr1 = ProcessRun( name="world", spec=LinkByUID(scope="test_scope", id="test_value"), uids={"test_scope": "another_test_value", "other_test": "also_valid" }, ) ms1 = MaterialSpec( name="material", process=LinkByUID(scope="test_scope", id="test_value"), uids={"second_scope": "this_is_an_id"}, ) mr1 = MaterialRun( name="material_run", spec=LinkByUID(scope="second_scope", id="this_is_an_id"), process=LinkByUID(scope="test_scope", id="another_test_value"), ) pr2 = ProcessRun( name="goodbye", spec=LinkByUID.from_entity(ps1), uids={"test_scope": "the_other_value"}, ) mr2 = MaterialRun( name="cruel", spec=LinkByUID.from_entity(ms1), process=LinkByUID.from_entity(pr2), ) gems = [ps1, pr1, ms1, mr1, pr2, mr2] gem_index = make_index(gems) for gem in gems: for scope in gem.uids: assert (scope, gem.uids[scope]) in gem_index assert gem_index[(scope, gem.uids[scope])] == gem # Make sure substitute_objects can consume the index subbed = substitute_objects(mr1, gem_index) assert subbed.spec.uids == ms1.uids
def make_node(name: str, *, process_name: str = None, process_template: ProcessTemplate = None, material_template: MaterialTemplate = None) -> MaterialRun: """ Generate a material-process spec-run quadruple. Parameters ---------- name: str Name of the MaterialRun and MaterialSpec. process_name: str Name of the ProcessRun and ProcessSpec. Defaults to `process_template.name` if `process_template` is defined, else `name`. process_template: ProcessTemplate ProcessTemplate for the quadruple. material_template: MaterialTemplate MaterialTemplate for the quadruple. Returns -------- MaterialRun A MaterialRun with linked processes, specs and templates """ if process_name is None: if process_template is None: process_name = name else: process_name = process_template.name my_process_spec = ProcessSpec( name=process_name, template=process_template ) my_process_run = ProcessRun( name=process_name, spec=my_process_spec ) my_mat_spec = MaterialSpec( name=name, process=my_process_spec, template=material_template ) my_mat_run = MaterialRun( name=name, process=my_process_run, spec=my_mat_spec ) return my_mat_run
def test_recursive_foreach(): """Test that recursive foreach will actually walk through a material history.""" mat_run = MaterialRun("foo") process_run = ProcessRun("bar") IngredientRun(process=process_run, material=mat_run) output = MaterialRun("material", process=process_run) # property templates are trickier than templates because they are referenced in attributes template = PropertyTemplate("prop", bounds=RealBounds(0, 1, "")) prop = Property("prop", value=NominalReal(1.0, ""), template=template) MeasurementRun("check", material=output, properties=prop) types = [] recursive_foreach(output, lambda x: types.append(x.typ)) expected = [ "ingredient_run", "material_run", "material_run", "process_run", "measurement_run", "property_template" ] assert sorted(types) == sorted(expected)
def test_link_by_uid(): """Test that linking works.""" root = MaterialRun(name='root', process=ProcessRun(name='root proc')) leaf = MaterialRun(name='leaf', process=ProcessRun(name='leaf proc')) IngredientRun(process=root.process, material=leaf) IngredientRun(process=root.process, material=LinkByUID.from_entity(leaf, scope='id')) # Paranoid assertions about equality's symmetry since it's implemented in 2 places assert root.process.ingredients[0].material == root.process.ingredients[ 1].material assert root.process.ingredients[0].material.__eq__( root.process.ingredients[1].material) assert root.process.ingredients[1].material.__eq__( root.process.ingredients[0].material) # Verify hash collision on equal LinkByUIDs assert LinkByUID.from_entity(leaf) in {LinkByUID.from_entity(leaf)} copy = loads(dumps(root)) assert copy.process.ingredients[0].material == copy.process.ingredients[ 1].material
def test_process_reassignment(): """Test that a material can be assigned to a new process.""" drying = ProcessRun("drying") welding = ProcessRun("welding") powder = MaterialRun("Powder", process=welding) assert powder.process == welding assert welding.output_material == powder powder.process = drying assert powder.process == drying assert drying.output_material == powder assert welding.output_material is None
def test_process_run(): """Test that a process run can house a material, and that it survives serde.""" process_run = ProcessRun("Bake a cake", uids={'My_ID': str(17)}) material_run = MaterialRun("A cake", process=process_run) # Check that a bi-directional link is established assert material_run.process == process_run assert process_run.output_material == material_run copy_material = loads(dumps(material_run)) assert dumps(copy_material) == dumps(material_run) assert 'output_material' in repr(process_run) assert 'process' in repr(material_run)
def test_many_ingredients(): """Test that ingredients remain connected to processes when round-robined through json.""" proc = ProcessRun("foo", spec=ProcessSpec("sfoo")) expected = [] for i in range(10): mat = MaterialRun(name=str(i), spec=MaterialSpec("s{}".format(i))) i_spec = IngredientSpec(name="i{}".format(i), material=mat.spec, process=proc.spec) IngredientRun(process=proc, material=mat, spec=i_spec) expected.append("i{}".format(i)) reloaded = loads(dumps(proc)) assert len(list(reloaded.ingredients)) == 10 names = [x.name for x in reloaded.ingredients] assert sorted(names) == sorted(expected)
def test_ingredient_run(): """Tests that a process can house an ingredient, and that pairing survives serialization.""" # Create a ProcessSpec proc_run = ProcessRun(name="a process spec", tags=["tag1", "tag2"]) ingred_run = IngredientRun(material=MaterialRun(name='Raw'), process=proc_run) # Make copies of both specs proc_run_copy = loads(dumps(proc_run)) assert proc_run_copy == proc_run, "Full structure wasn't preserved across serialization" assert 'process' in repr(ingred_run) assert 'ingredients' in repr(proc_run)
def test_equality(): """Test that equality check works as expected.""" spec = MaterialSpec("A spec", properties=PropertyAndConditions( property=Property("a property", value=NominalReal(3, ''))), tags=["a tag"]) mat1 = MaterialRun("A material", spec=spec) mat2 = MaterialRun("A material", spec=spec, tags=["A tag"]) assert mat1 == deepcopy(mat1) assert mat1 != mat2 assert mat1 != "A material" mat3 = deepcopy(mat1) assert mat1 == mat3, "Copy somehow failed" MeasurementRun("A measurement", material=mat3) assert mat1 != mat3 mat4 = deepcopy(mat3) assert mat4 == mat3, "Copy somehow failed" mat4.measurements[0].tags.append('A tag') assert mat4 != mat3 mat5 = next(x for x in flatten(mat4, 'test-scope') if isinstance(x, MaterialRun)) assert mat5 == mat4, "Flattening removes measurement references, but that's okay"
def test_dictionary_substitution(): """substitute_objects() should substitute LinkByUIDs that occur in dict keys and values.""" proc = ProcessRun("A process", uids={'id': '123'}) mat = MaterialRun("A material", uids={'generic id': '38f8jf'}) proc_link = LinkByUID.from_entity(proc) mat_link = LinkByUID.from_entity(mat) index = {(mat_link.scope.lower(), mat_link.id): mat, (proc_link.scope.lower(), proc_link.id): proc} test_dict = {LinkByUID.from_entity(proc): LinkByUID.from_entity(mat)} subbed = substitute_objects(test_dict, index) k, v = next((k, v) for k, v in subbed.items()) assert k == proc assert v == mat
def test_more_iterable_types(): """Verify recursive_foreach behaves for additional iterable types.""" obj = MaterialRun("foo", tags=["1", "2", "3"]) assert "1" in obj.tags recursive_foreach({obj}, lambda x: x.tags.remove("1")) assert "1" not in obj.tags dct = {obj: obj} assert "2" in obj.tags recursive_foreach(dct.keys(), lambda x: x.tags.remove("2")) assert "2" not in obj.tags assert "3" in obj.tags recursive_foreach(dct.values(), lambda x: x.tags.remove("3")) assert "3" not in obj.tags
def test_scope_substitution(): """Test that the native id gets serialized, when specified.""" native_id = 'id1' # Create measurement and material with two ids mat = MaterialRun("A material", uids={ native_id: str(uuid4()), "an_id": str(uuid4()), "another_id": str(uuid4())}) meas = MeasurementRun("A measurement", material=mat, uids={ "some_id": str(uuid4()), native_id: str(uuid4()), "an_id": str(uuid4())}) # Turn the material pointer into a LinkByUID using native_id subbed = substitute_links(meas, scope=native_id) assert subbed.material == LinkByUID.from_entity(mat, scope=native_id) # Put the measurement into a list and convert that into a LinkByUID using native_id measurements_list = [meas] subbed = substitute_links(measurements_list, scope=native_id) assert subbed == [LinkByUID.from_entity(meas, scope=native_id)]
def test_material_soft_link(): """Test that a measurement run can link to a material run, and that it survives serde.""" dye = MaterialRun("rhodamine", file_links=FileLink(filename='a.csv', url='/a/path')) assert dye.measurements == [], "default value of .measurements should be an empty list" # The .measurements member should not be settable with pytest.raises(AttributeError): dye.measurements = [MeasurementRun("Dummy")] absorbance = MeasurementRun(name="Absorbance", uids={'id': str(uuid4())}, properties=[ Property(name='Abs at 500 nm', value=NominalReal(0.1, '')) ]) assert absorbance.material is None, "Measurements should have None as the material by default" absorbance.material = dye assert absorbance.material == dye, "Material not set correctly for measurement" assert dye.measurements == [ absorbance ], "Soft-link from material to measurement not created" fluorescence = MeasurementRun(name="Fluorescence", uids={'id': str(uuid4())}, properties=[ Property(name='PL counts at 550 nm', value=NominalReal(30000, '')) ], material=dye) assert fluorescence.material == dye, "Material not set correctly for measurement" assert dye.measurements == [absorbance, fluorescence], \ "Soft-link from material to measurements not created" assert loads(dumps(absorbance)) == absorbance, \ "Measurement should remain unchanged when serialized" assert loads(dumps(fluorescence)) == fluorescence, \ "Measurement should remain unchanged when serialized" assert 'measurements' in repr(dye) assert 'material' in repr(fluorescence) assert 'material' in repr(absorbance) subbed = substitute_links(dye) assert 'measurements' in repr(subbed)
def test_table(): """Convert a dictionary to a pandas dataframe and then to a table.""" material = MaterialRun("name") df = pd.DataFrame.from_records(data) result = ingest_table(material, df) assert isinstance(result, MaterialRun) assert len(result.measurements) == len(data) filename = "/tmp/table_example.json" # write to a file (for human inspection) with open(filename, "w") as f: dump(result, f, indent=2) # read it back with open(filename, "r") as f: copy = load(f) assert isinstance(copy, MaterialRun)
def test_from_entity(): """Test permutations of LinkByUID.from_entity arguments.""" run = MaterialRun(name='leaf', process=ProcessRun(name='leaf proc')) assert LinkByUID.from_entity(run).scope == 'auto' assert LinkByUID.from_entity(run, scope='missing').scope == 'auto' assert len(run.uids) == 1 run.uids['foo'] = 'bar' link1 = LinkByUID.from_entity(run, scope='foo') assert (link1.scope, link1.id) == ('foo', 'bar') with pytest.deprecated_call(): assert LinkByUID.from_entity(run, 'foo').scope == 'foo' with pytest.deprecated_call(): assert LinkByUID.from_entity(run, name='foo').scope == 'foo' with pytest.raises(ValueError): LinkByUID.from_entity(run, name='scope1', scope='scope2')
def test_thin_dumps(): """Test that thin_dumps turns pointers into links.""" mat = MaterialRun("The actual material") meas_spec = MeasurementSpec("measurement", uids={'my_scope': '324324'}) meas = MeasurementRun("The measurement", spec=meas_spec, material=mat) thin_copy = MeasurementRun.build(json.loads(GEMDJson().thin_dumps(meas))) assert isinstance(thin_copy, MeasurementRun) assert isinstance(thin_copy.material, LinkByUID) assert isinstance(thin_copy.spec, LinkByUID) assert thin_copy.spec.id == meas_spec.uids['my_scope'] # Check that LinkByUID objects are correctly converted their JSON equivalent expected_json = '{"id": "my_id", "scope": "scope", "type": "link_by_uid"}' assert GEMDJson().thin_dumps(LinkByUID('scope', 'my_id')) == expected_json # Check that objects lacking .uid attributes will raise an exception when dumped with pytest.raises(TypeError): GEMDJson().thin_dumps({{'key': 'value'}})