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_template_access(): """A process run's template should be equal to its spec's template.""" template = ProcessTemplate("process template", uids={'id': str(uuid4())}) spec = ProcessSpec("A spec", uids={'id': str(uuid4())}, template=template) proc = ProcessRun("A run", uids={'id': str(uuid4())}, spec=spec) assert proc.template == template proc.spec = LinkByUID.from_entity(spec) assert proc.template is None
def test_equality(): """Test that the __eq__ method performs as expected.""" link = LinkByUID(scope="foo", id="bar") assert link == ProcessRun("Good", uids={"foo": "bar"}) assert link != ProcessRun("Good", uids={"foo": "rab"}) assert link != ProcessRun("Good", uids={"oof": "bar"}) assert link != LinkByUID(scope="foo", id="rab") assert link == ("foo", "bar") assert link != ("foo", "bar", "baz") assert link != ("foo", "rab")
def test_inplace_v_not(): """Test that client can copy a dictionary in which keys are BaseEntity objects.""" spec = ProcessSpec("A process spec", uids={'id': str(uuid4()), 'auto': str(uuid4())}) run1 = ProcessRun("A process run", spec=spec, uids={'id': str(uuid4()), 'auto': str(uuid4())}) run2 = ProcessRun("Another process run", spec=spec, uids={'id': str(uuid4())}) process_dict = {spec: [run1, run2]} subbed = substitute_links(process_dict) assert subbed != process_dict # This is true because the hashes change, even if objects equal substitute_links(process_dict, inplace=True) assert subbed == process_dict
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_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_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_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_flatmap_unidirectional_ordering(): """Test that the unidirecitonal setting is obeyed.""" # The writeable link is ingredient -> process, not process -> ingredients proc = ProcessRun(name="foo") IngredientRun(notes="bar", process=proc) assert len(recursive_flatmap(proc, lambda x: [x], unidirectional=False)) == 2 assert len(recursive_flatmap(proc, lambda x: [x], unidirectional=True)) == 0
def test_tuple_sub(): """substitute_objects() should correctly substitute tuple values.""" proc = ProcessRun('foo', uids={'id': '123'}) proc_link = LinkByUID.from_entity(proc) index = {(proc_link.scope, proc_link.id): proc} tup = (proc_link,) subbed = substitute_objects(tup, index) assert subbed[0] == proc
def test_default_scope(): """Test flatten exceptions around providing a scope.""" ps_one = ProcessSpec(name="one") with pytest.raises(ValueError): flatten(ps_one) pr_one = ProcessRun(name="one", uids={'my': 'outer'}, spec=ps_one) with pytest.raises(ValueError): flatten(pr_one) ps_one.uids['my'] = 'id' assert len(flatten(pr_one)) == 2 two = ProcessRun(name="two", spec=ProcessSpec(name="two")) assert len(flatten(two, scope="my")) == 2
def test_unexpected_serialization(): """Trying to serialize an unexpected class should throw a TypeError.""" class DummyClass: def __init__(self, foo): self.foo = foo with pytest.raises(TypeError): dumps(ProcessRun("A process", notes=DummyClass("something")))
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_object_key_substitution(): """Test that client can copy a dictionary in which keys are BaseEntity objects.""" spec = ProcessSpec("A process spec", uids={'id': str(uuid4()), 'auto': str(uuid4())}) run1 = ProcessRun("A process run", spec=spec, uids={'id': str(uuid4()), 'auto': str(uuid4())}) run2 = ProcessRun("Another process run", spec=spec, uids={'id': str(uuid4())}) process_dict = {spec: [run1, run2]} subbed = substitute_links(process_dict, scope='auto') for key, value in subbed.items(): assert key == LinkByUID.from_entity(spec, scope='auto') assert LinkByUID.from_entity(run1, scope='auto') in value assert LinkByUID.from_entity(run2) in value reverse_process_dict = {run2: spec} subbed = substitute_links(reverse_process_dict, scope='auto') for key, value in subbed.items(): assert key == LinkByUID.from_entity(run2) assert value == LinkByUID.from_entity(spec, scope='auto')
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 test_substitute_equivalence(): """PLA-6423: verify that substitutions match up.""" spec = ProcessSpec(name="old spec", uids={'scope': 'spec'}) run = ProcessRun(name="old run", uids={'scope': 'run'}, spec=LinkByUID(id='spec', scope="scope")) # make a dictionary from ids to objects, to be used in substitute_objects gem_index = make_index([run, spec]) substitute_objects(obj=run, index=gem_index, inplace=True) assert spec == run.spec
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_unexpected_deserialization(): """Trying to deserialize an unexpected class should throw a TypeError.""" class DummyClass(DictSerializable): typ = 'dummy_class' def __init__(self, foo): self.foo = foo # DummyClass cannot be serialized since dumps will round-robin serialize # in the substitute_links method with pytest.raises(TypeError): dumps(ProcessRun("A process", notes=DummyClass("something")))
def test_flatten_empty_history(): """Test that flatten works when the objects are empty and go through a whole history.""" procured = ProcessSpec(name="procured") input = MaterialSpec(name="foo", process=procured) transform = ProcessSpec(name="transformed") ingredient = IngredientSpec(name="input", material=input, process=transform) procured_run = ProcessRun(name="procured", spec=procured) input_run = MaterialRun(name="foo", process=procured_run, spec=input) transform_run = ProcessRun(name="transformed", spec=transform) ingredient_run = IngredientRun(material=input_run, process=transform_run, spec=ingredient) assert len(flatten(procured)) == 1 assert len(flatten(input)) == 1 assert len(flatten(ingredient)) == 3 assert len(flatten(transform)) == 3 assert len(flatten(procured_run)) == 3 assert len(flatten(input_run)) == 3 assert len(flatten(ingredient_run)) == 7 assert len(flatten(transform_run)) == 7
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_signature(): """Exercise various permutations of the substitute_links sig.""" spec = ProcessSpec("A process spec", uids={'my': 'spec'}) with pytest.warns(DeprecationWarning): run1 = ProcessRun("First process run", uids={'my': 'run1'}, spec=spec) assert isinstance(substitute_links(run1, native_uid='my').spec, LinkByUID) run2 = ProcessRun("Second process run", uids={'my': 'run2'}, spec=spec) assert isinstance(substitute_links(run2, scope='my').spec, LinkByUID) run3 = ProcessRun("Third process run", uids={'my': 'run3'}, spec=spec) assert isinstance(substitute_links(run3, 'my').spec, LinkByUID) with pytest.raises(ValueError): # Test deprecated auto-population run4 = ProcessRun("Fourth process run", uids={'my': 'run4'}, spec=spec) assert isinstance(substitute_links(run4, 'other', allow_fallback=False).spec, LinkByUID) with pytest.warns(DeprecationWarning): with pytest.raises(ValueError): # Test deprecated auto-population run5 = ProcessRun("Fifth process run", uids={'my': 'run4'}, spec=spec) assert isinstance(substitute_links(run5, scope="my", native_uid="my").spec, LinkByUID)
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_process_spec(): """Tests that the Process Spec/Run connection persists when serializing.""" # Create the ProcessSpec condition1 = Condition(name="a condition on the process in general") spec = ProcessSpec("Spec", conditions=condition1) # Create the ProcessRun with a link to the ProcessSpec from above condition2 = Condition( name="a condition on this process run in particular") process = ProcessRun("Run", conditions=condition2, spec=spec) copy_process = loads(dumps(process)) assert dumps(copy_process.spec) == dumps(spec), \ "Process spec should be preserved through serialization"
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_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_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_equality(): """Test that equality check works as expected.""" spec = ProcessSpec("A spec", tags=["a tag"]) run1 = ProcessRun("A process", spec=spec) run2 = deepcopy(run1) assert run1 == run2, "Copy somehow failed" IngredientRun(process=run2) assert run1 != run2 run3 = deepcopy(run2) assert run3 == run2, "Copy somehow failed" run3.ingredients[0].tags.append('A tag') assert run3 != run2 run4 = next(x for x in flatten(run3, 'test-scope') if isinstance(x, ProcessRun)) assert run4 == run3, "Flattening removes measurement references, but that's okay"
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_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)