def test_sac(): """Make S&C table and assert that it can be serialized.""" sac = make_strehlow_objects(import_table()) sac_tbl = make_strehlow_table(sac) # Check that all shapes of records serialize and deserialize for comp in sac: assert je.loads(je.dumps(comp)) == comp # Verify that specs are shared when compounds match for comp1 in sac: for comp2 in sac: # xand assert (comp1.name == comp2.name) == (comp1.spec.uids == comp2.spec.uids) # Look at each different combination of Value types in a S&C record smaller = minimal_subset(sac_tbl['content']) # Make sure that the diversity of value types isn't lost, e.g. something is being None'd assert len(smaller) == 162 # Make sure there's no migration with repeated serialization for row in sac_tbl: assert je.dumps(je.loads(je.dumps(row))) == je.dumps(row) # Verify that the serialization trick for mocking a structured table works json.dumps(json.loads(je.dumps(sac_tbl))["object"], indent=2)
def test_material_spec(): """Test that Process/Material Spec link survives serialization.""" # Create a ProcessSpec proc_spec = ProcessSpec(name="a process spec", tags=["tag1", "tag2"]) # Create MaterialSpec without a ProcessSpec prop = Property(name="The material is a solid", value=DiscreteCategorical(probabilities="solid")) mat_spec = MaterialSpec(name="a material spec", properties=PropertyAndConditions(prop)) assert mat_spec.process is None, \ "MaterialSpec should be initialized with no ProcessSpec, by default" # Assign a ProcessSpec to mat_spec, first ensuring that the type is enforced with pytest.raises(TypeError): mat_spec.process = 17 mat_spec.process = proc_spec # Assert circular links assert dumps(proc_spec.output_material.process) == dumps(proc_spec), \ "ProcessSpec should link to MaterialSpec that links back to itself" assert dumps(mat_spec.process.output_material) == dumps(mat_spec), \ "MaterialSpec should link to ProcessSpec that links back to itself" # Make copies of both specs mat_spec_copy = loads(dumps(mat_spec)) proc_spec_copy = loads(dumps(proc_spec)) assert proc_spec_copy.output_material == mat_spec, \ "Serialization should preserve link from ProcessSpec to MaterialSpec" assert mat_spec_copy.process == proc_spec, \ "Serialization should preserve link from MaterialSpec to ProcessSpec"
def test_attribute_serde(): """An attribute with a link to an attribute template should be copy-able.""" prop_tmpl = PropertyTemplate(name='prop_tmpl', bounds=RealBounds(0, 2, 'm')) prop = Property(name='prop', template=prop_tmpl, value=NominalReal(1, 'm')) meas_spec = MeasurementSpec("a spec") meas = MeasurementRun("a measurement", spec=meas_spec, properties=[prop]) assert loads(dumps(prop)) == prop assert loads(dumps(meas)) == meas assert isinstance(prop.template, PropertyTemplate)
def test_numpy(): """Test that ndarrays, Series work as well.""" assert len(array_like()) < 5 # In case we extend at some point if len(array_like()) > 2: # Test numpy import numpy as np np_bounds = CategoricalBounds(np.array(["spam", "eggs"], dtype=object)) np_copy = loads(dumps(np_bounds)) assert np_copy == np_bounds if len(array_like()) > 3: # Test numpy import pandas as pd pd_bounds = CategoricalBounds(pd.Series(["spam", "eggs"])) pd_copy = loads(dumps(pd_bounds)) assert pd_copy == pd_bounds
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_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_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_passthrough_bounds(): """Test that unspecified Bounds are accepted and set to None.""" template = ProcessTemplate('foo', conditions=[ (LinkByUID('1', '2'), None), [LinkByUID('3', '4'), None], LinkByUID('5', '6'), ConditionTemplate('foo', bounds=IntegerBounds( 0, 10)), ]) assert len(template.conditions) == 4 for _, bounds in template.conditions: assert bounds is None copied = loads(dumps(template)) assert len(copied.conditions) == 4 for _, bounds in copied.conditions: assert bounds is None from_dict = ProcessTemplate.build({ 'type': 'process_template', 'name': 'foo', 'conditions': [[ { 'scope': 'foo', 'id': 'bar', 'type': 'link_by_uid', }, None, ]], }) assert len(from_dict.conditions) == 1
def test_recursive_equals(): """Verify that the recursive/crawling equals behaves well.""" cake = make_cake() copy = loads(dumps(cake)) assert cake == copy copy.process.ingredients[0].material.process.ingredients[0].material.tags.append('Hi') assert cake != copy
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_json_serde(): """Test that values can be ser/de using our custom json loads/dumps.""" # Enums are only used in the context of another class -- # it is not possible to deserialize to enum with the current # serialization strategy (plain string) without this context. original = Property(name="foo", origin=Origin.MEASURED) copy = loads(dumps(original)) assert original == copy
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_source(): """Test that source can be set, serialized, and deserialized.""" source = PerformedSource(performed_by="Marie Curie", performed_date="1898-07-01") measurement = MeasurementRun(name="Polonium", source=source) assert loads(dumps(measurement)).source.performed_by == "Marie Curie" with pytest.raises(TypeError): MeasurementRun(name="Polonium", source="Marie Curie on 1898-07-01")
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)) copy = loads(dumps(root)) assert copy.process.ingredients[0].material == copy.process.ingredients[ 1].material
def test_condition_template(): """Test creation and serde of condition templates.""" bounds = RealBounds(2.5, 10.0, default_units='cm') template = ConditionTemplate("Chamber width", bounds=bounds, description="width of chamber") assert template.uids is not None # uids should be added automatically # Take template through a serde cycle and ensure that it is unchanged assert ConditionTemplate.build(template.dump()) == template # A more complicated cycle that goes through both gemd-python and citrine-python serde. assert ConditionTemplate.build(loads(dumps(template.dump())).as_dict()) == template
def test_ingredient_spec(): """Tests that a process can house an ingredient, and that pairing survives serialization.""" # Create a ProcessSpec proc_spec = ProcessSpec(name="a process spec", tags=["tag1", "tag2"]) IngredientSpec(name='Input', material=MaterialSpec(name='Raw'), process=proc_spec) # Make copies of both specs proc_spec_copy = loads(dumps(proc_spec)) assert proc_spec_copy == proc_spec, "Full structure wasn't preserved across serialization"
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_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(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(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_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_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_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', uids={'id': '12345'}) 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_measurement_spec(): """Test the measurement spec/run connection survives ser/de.""" condition = Condition(name="Temp condition", value=NominalReal(nominal=298, units='kelvin')) parameter = Parameter(name="Important parameter") spec = MeasurementSpec(name="Precise way to do a measurement", parameters=parameter, conditions=condition) # Create a measurement run from this measurement spec measurement = MeasurementRun("The Measurement", conditions=condition, spec=spec) copy = loads(dumps(measurement)) assert dumps(copy.spec) == dumps(measurement.spec), \ "Measurement spec should be preserved if measurement run is serialized"
def test_case_insensitive_rehydration(): """ Test that loads() can connect id scopes with different cases. This situation should not occur in gemd on its own, but faraday returns LinkOrElse objects with the default scope "ID", whereas citrine-python assigns ids with the scope "id". The uids dictionary is supposed to be case-insensitive, so rehydration should still work. """ # A simple json string that could be loaded, representing an ingredient linked to a material. # The material link has "scope": "ID", whereas the material in the context list, which is # to be loaded, has uid with scope "id". json_str = ''' { "context": [ { "uids": { "id": "9118c2d3-1c38-47fe-a650-c2b92fdb6777" }, "type": "material_run", "name": "flour" } ], "object": { "type": "ingredient_run", "uids": { "id": "8858805f-ec02-49e4-ba3b-d784e2aea3f8" }, "material": { "type": "link_by_uid", "scope": "ID", "id": "9118c2d3-1c38-47fe-a650-c2b92fdb6777" }, "process": { "type": "link_by_uid", "scope": "ID", "id": "9148c2d3-2c38-47fe-b650-c2b92fdb6777" } } } ''' loaded_ingredient = loads(json_str) # The ingredient's material will either be a MaterialRun (pass) or a LinkByUID (fail) assert isinstance(loaded_ingredient.material, MaterialRun)
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_material_id_link(): """Check that a measurement can be linked to a material that is a LinkByUID.""" mat = LinkByUID('id', str(uuid4())) meas = MeasurementRun("name", material=mat) assert meas.material == mat assert loads(dumps(meas)) == meas
def test_deeply_nested_rehydration(): """ Tests that loads fully replaces links with objects. In particular, this test makes sure that loads is robust to objects being referenced by LinkByUid before they are "declared" in the JSON array. """ json_str = ''' { "context": [ { "type": "process_spec", "parameters": [ { "type": "parameter", "name": "oven", "value": { "type": "nominal_categorical", "category": "oven 1" }, "template": { "type": "link_by_uid", "scope": "id", "id": "536a3ebb-55a4-4560-a6df-fba44cdb917a" }, "origin": "unknown", "file_links": [] } ], "conditions": [], "uids": { "id": "f77dc327-ef44-4a39-a617-061ace5fa789" }, "tags": [], "name": "Ideal baking", "file_links": [] }, { "type": "process_run", "spec": { "type": "link_by_uid", "scope": "id", "id": "f77dc327-ef44-4a39-a617-061ace5fa789" }, "parameters": [ { "type": "parameter", "name": "oven", "value": { "type": "nominal_categorical", "category": "oven 1" }, "template": { "type": "link_by_uid", "scope": "id", "id": "536a3ebb-55a4-4560-a6df-fba44cdb917a" }, "origin": "unknown", "file_links": [] } ], "conditions": [], "uids": { "id": "7cb9471b-0c90-4fd9-bfe1-0e9d7602ab0d", "my_id": "jvkzrlnm" }, "tags": [ "cake::yes" ], "name": "cake baking", "file_links": [] }, { "type": "material_spec", "properties": [], "uids": { "id": "230fc837-8a19-402c-86ad-e451b7a80f9d" }, "tags": [], "name": "Flour", "file_links": [] }, { "type": "material_spec", "process": { "type": "link_by_uid", "scope": "id", "id": "f77dc327-ef44-4a39-a617-061ace5fa789" }, "properties": [], "uids": { "id": "b935aa7d-93a4-407f-937f-cca32d7a8413" }, "tags": [], "name": "An ideal cake", "file_links": [] }, { "type": "material_spec", "properties": [ { "property": { "type": "property", "name": "mass", "value": { "type": "normal_real", "mean": 0.84, "std": 0.04, "units": "gram" }, "template": { "type": "link_by_uid", "scope": "id", "id": "3b46b191-b3d0-4b31-bdba-377cca315cbd" }, "origin": "unknown", "file_links": [] }, "conditions": [ { "type": "condition", "name": "temperature", "value": { "type": "nominal_real", "nominal": 20, "units": "degC" }, "template": { "type": "link_by_uid", "scope": "id", "id": "09fb94ab-17fb-4428-a20e-d6b0d0ae5fb2" }, "origin": "unknown", "file_links": [] } ], "type": "property_and_conditions" } ], "uids": { "id": "39ec0605-0b9b-443c-ab6a-4d7bc1b73b24" }, "tags": [], "name": "Butter", "file_links": [] }, { "type": "ingredient_spec", "material": { "type": "link_by_uid", "scope": "id", "id": "39ec0605-0b9b-443c-ab6a-4d7bc1b73b24" }, "process": { "type": "link_by_uid", "scope": "id", "id": "f77dc327-ef44-4a39-a617-061ace5fa789" }, "labels": [], "uids": { "id": "118eacb7-6edc-4e57-b40b-2971481d37e5" }, "tags": [], "file_links": [] }, { "type": "ingredient_spec", "material": { "type": "link_by_uid", "scope": "id", "id": "230fc837-8a19-402c-86ad-e451b7a80f9d" }, "process": { "type": "link_by_uid", "scope": "id", "id": "f77dc327-ef44-4a39-a617-061ace5fa789" }, "labels": [], "absolute_quantity": { "type": "normal_real", "mean": 500, "std": 50, "units": "gram" }, "uids": { "id": "f694d2cc-5b00-42ef-92b7-dee3cdc7239a" }, "tags": [], "file_links": [] }, { "type": "material_run", "spec": { "type": "link_by_uid", "scope": "id", "id": "230fc837-8a19-402c-86ad-e451b7a80f9d" }, "sample_type": "unknown", "uids": { "id": "76185e4f-c778-4654-a2ae-cc49851e291f" }, "tags": [], "name": "Flour", "file_links": [] }, { "type": "material_run", "spec": { "type": "link_by_uid", "scope": "id", "id": "39ec0605-0b9b-443c-ab6a-4d7bc1b73b24" }, "sample_type": "unknown", "uids": { "id": "605bf096-3b2d-4c3b-afaf-f77bcff9806f" }, "tags": [], "name": "Butter", "file_links": [] },{ "type": "material_run", "process": { "type": "link_by_uid", "scope": "id", "id": "7cb9471b-0c90-4fd9-bfe1-0e9d7602ab0d" }, "spec": { "type": "link_by_uid", "scope": "id", "id": "b935aa7d-93a4-407f-937f-cca32d7a8413" }, "sample_type": "unknown", "uids": { "id": "f0f41fb9-32dc-4903-aaf4-f369de71530f" }, "tags": [], "name": "A cake", "file_links": [] }, { "type": "ingredient_run", "material": { "type": "link_by_uid", "scope": "id", "id": "76185e4f-c778-4654-a2ae-cc49851e291f" }, "process": { "type": "link_by_uid", "scope": "id", "id": "7cb9471b-0c90-4fd9-bfe1-0e9d7602ab0d" }, "spec": { "type": "link_by_uid", "scope": "id", "id": "f694d2cc-5b00-42ef-92b7-dee3cdc7239a" }, "name": "500 g flour", "labels": [], "uids": { "id": "36aa5bff-c89d-43fa-95c8-fa6b710061d8" }, "tags": [], "file_links": [] }, { "type": "ingredient_run", "material": { "type": "link_by_uid", "scope": "id", "id": "605bf096-3b2d-4c3b-afaf-f77bcff9806f" }, "process": { "type": "link_by_uid", "scope": "id", "id": "7cb9471b-0c90-4fd9-bfe1-0e9d7602ab0d" }, "spec": { "type": "link_by_uid", "scope": "id", "id": "118eacb7-6edc-4e57-b40b-2971481d37e5" }, "name": "1 stick butter", "labels": [], "absolute_quantity": { "type": "nominal_real", "nominal": 1, "units": "dimensionless" }, "uids": { "id": "91ab45f2-ceec-4109-8f74-2f9964a4bc2c" }, "tags": [], "file_links": [] }, { "type": "measurement_spec", "parameters": [], "conditions": [], "uids": { "id": "85c911eb-af5a-4c34-9b59-b88b84780239" }, "tags": [], "name": "Tasty spec", "file_links": [] }, { "type": "measurement_run", "spec": { "type": "link_by_uid", "scope": "id", "id": "85c911eb-af5a-4c34-9b59-b88b84780239" }, "material": { "type": "link_by_uid", "scope": "id", "id": "f0f41fb9-32dc-4903-aaf4-f369de71530f" }, "properties": [], "parameters": [], "conditions": [], "uids": { "id": "9673f15d-76df-4dcd-a409-7152cb629a3f" }, "tags": [ "example::tag" ], "name": "Tastiness", "notes": "it is tasty", "file_links": [] } ], "object": { "type": "link_by_uid", "scope": "id", "id": "f0f41fb9-32dc-4903-aaf4-f369de71530f" } } ''' material_history = loads(json_str) assert isinstance(material_history.process.ingredients[1].spec, IngredientSpec) assert isinstance(material_history.measurements[0], MeasurementRun) copied = loads(dumps(material_history)) assert isinstance(copied.process.ingredients[1].spec, IngredientSpec) assert isinstance(copied.measurements[0], MeasurementRun)
def test_deserialize_extra_fields(): """Extra JSON fields should be ignored in deserialization.""" json_data = '{"context": [],' \ ' "object": {"nominal": 5, "type": "nominal_integer", "extra garbage": "foo"}}' assert(loads(json_data) == NominalInteger(nominal=5))
def test_json(): """Test that serialization works (empty dictionary).""" bounds = MolecularStructureBounds() copy = loads(dumps(bounds)) assert copy == bounds
def test_for_loss(obj): assert (obj == loads(dumps(obj)))
def test_cake(): """Create cake, serialize, deserialize.""" cake = make_cake() def test_for_loss(obj): assert (obj == loads(dumps(obj))) recursive_foreach(cake, test_for_loss) # And verify equality was working in the first place cake2 = loads(dumps(cake)) cake2.name = "It's a trap!" assert (cake2 != cake) cake2.name = cake.name assert (cake == cake2) cake2.uids['new'] = "It's a trap!" assert (cake2 != cake) # Check that all the objects show up tot_count = 0 def increment(dummy): nonlocal tot_count tot_count += 1 recursive_foreach(cake, increment) assert tot_count == 131 # And make sure nothing was lost tot_count = 0 recursive_foreach(loads(dumps(complete_material_history(cake))), increment) assert tot_count == 131 # Check that no UIDs collide uid_seen = dict() def check_ids(obj): nonlocal uid_seen for scope in obj.uids: lbl = '{}::{}'.format(scope, obj.uids[scope]) if lbl in uid_seen: assert uid_seen[lbl] == id(obj) uid_seen[lbl] = id(obj) recursive_foreach(cake, check_ids) queue = [cake] seen = set() while queue: obj = queue.pop() if obj in seen: continue seen.add(obj) if isinstance(obj, MaterialSpec): if obj.process is not None: queue.append(obj.process) assert obj.process.output_material == obj elif isinstance(obj, MaterialRun): if obj.process is not None: queue.append(obj.process) assert obj.process.output_material == obj if obj.measurements: queue.extend(obj.measurements) for msr in obj.measurements: assert msr.material == obj if obj.spec is not None: queue.append(obj.spec) if obj.process is not None: assert obj.spec.process == obj.process.spec elif isinstance(obj, ProcessRun): if obj.ingredients: queue.extend(obj.ingredients) if obj.output_material is not None: queue.append(obj.output_material) assert obj.output_material.process == obj if obj.spec is not None: assert obj.spec.output_material == obj.output_material.spec elif isinstance(obj, ProcessSpec): if obj.ingredients: queue.extend(obj.ingredients) if obj.output_material is not None: queue.append(obj.output_material) assert obj.output_material.process == obj elif isinstance(obj, MeasurementSpec): pass # Doesn't link elif isinstance(obj, MeasurementRun): if obj.spec: queue.append(obj.spec) elif isinstance(obj, IngredientSpec): if obj.material: queue.append(obj.material) elif isinstance(obj, IngredientRun): if obj.spec: queue.append(obj.spec) if obj.material and isinstance(obj.material, MaterialRun): assert obj.spec.material == obj.material.spec if obj.material: queue.append(obj.material)