예제 #1
0
 def test_caput_status_pv_message(self, catools):
     p = self.create_part(
         dict(
             name="mname",
             description="desc",
             pv="pv",
             status_pv="spv",
             good_status="All Good",
             message_pv="mpv",
         )
     )
     catools.caget.return_value = [caint(4)]
     c = StatefulController("mri")
     c.add_part(p)
     proc = Process("proc")
     proc.add_controller(c)
     proc.start()
     self.addCleanup(proc.stop)
     b = proc.block_view("mri")
     self.make_camonitor_return(catools, "No Good")
     catools.caget.return_value = "Bad things happened"
     with self.assertRaises(AssertionError) as cm:
         b.mname()
     assert (
         str(cm.exception) == "Status No Good: Bad things happened: "
         "while performing 'caput pv 1'"
     )
 def setUp(self):
     self.process = Process("proc")
     self.o = RawMotorSinkPortsPart("PV:PRE")
     c = StatefulController("mri")
     c.add_part(self.o)
     self.process.add_controller(c)
     self.b = self.process.block_view("mri")
     self.addCleanup(self.process.stop)
예제 #3
0
 def setUp(self):
     self.process = Process("proc")
     self.params = Mock()
     self.params.mri = "MyMRI"
     self.params.description = "My description"
     self.part = MyPart("testpart")
     self.o = StatefulController(self.process, [self.part], self.params)
     self.process.add_controller(self.params.mri, self.o)
예제 #4
0
 def setUp(self):
     self.process = Process("proc")
     self.part = MyPart("testpart")
     self.part2 = MyPart("testpart2")
     self.o = StatefulController("MyMRI")
     self.o.add_part(self.part)
     self.o.add_part(self.part2)
     self.process.add_controller(self.o)
     self.b = self.process.block_view("MyMRI")
 def setUp(self, catools):
     self.catools = catools
     catools.caget.side_effect = [[castr("@asyn(BRICK1CS1,2)")]]
     self.process = Process("proc")
     self.o = CompoundMotorCSPart("cs", "PV:PRE.OUT")
     c = StatefulController("mri")
     c.add_part(self.o)
     self.process.add_controller(c)
     self.b = self.process.block_view("mri")
     self.process.start()
     self.addCleanup(self.process.stop)
예제 #6
0
 def setUp(self, catools):
     self.catools = catools
     catools.caget.side_effect = [[castr("BRICK1CS1")]]
     self.process = Process("proc")
     self.o = CSSourcePortsPart("cs", "PV:PRE:Port")
     c = StatefulController("mri")
     c.add_part(self.o)
     self.process.add_controller(c)
     self.b = self.process.block_view("mri")
     self.process.start()
     self.addCleanup(self.process.stop)
 def _make_child_controller(self, parts, mri):
     # Add some extra parts to determine the dataset name and type for
     # any CAPTURE field part
     new_parts = []
     for existing_part in parts:
         new_parts.append(existing_part)
         if hasattr(existing_part, "field_name") and \
                 existing_part.field_name.endswith(".CAPTURE"):
             # Add capture dataset name and type
             part_name = existing_part.field_name.replace(
                 ".CAPTURE", ".DATASET_NAME")
             attr_name = snake_to_camel(part_name.replace(".", "_"))
             new_parts.append(
                 StringPart(
                     name=attr_name,
                     description="Name of the captured dataset in HDF file",
                     writeable=True))
             # Make a choice part to hold the type of the dataset
             part_name = existing_part.field_name.replace(
                 ".CAPTURE", ".DATASET_TYPE")
             attr_name = snake_to_camel(part_name.replace(".", "_"))
             if "INENC" in mri:
                 initial = "position"
             else:
                 initial = "monitor"
             new_parts.append(
                 ChoicePart(
                     name=attr_name,
                     description="Type of the captured dataset in HDF file",
                     writeable=True,
                     choices=list(AttributeDatasetType),
                     value=initial))
     if mri.endswith("PCAP"):
         cs, ps = adbase_parts(prefix=self.prefix)
         controller = StatefulController(mri=mri)
         for p in new_parts + ps:
             controller.add_part(p)
         for c in cs:
             self.process.add_controller(c)
     else:
         controller = super(PandABlocksRunnableController, self).\
             _make_child_controller(new_parts, mri)
     return controller
예제 #8
0
    def setUp(self):
        self.p = Process("process1")

        # create a child to client
        self.c_child = StatefulController("childBlock")
        self.c_part = MyPart("cp1")
        self.c_child.add_part(self.c_part)
        self.p.add_controller(self.c_child)

        # create a root block for the ManagerController block to reside in
        if os.path.isdir("/tmp/mainBlock"):
            shutil.rmtree("/tmp/mainBlock")
        self.c = ManagerController("mainBlock", config_dir="/tmp")
        self.c.add_part(MyPart("part1"))
        self.c.add_part(ChildPart("part2", mri="childBlock", initial_visibility=True))
        self.p.add_controller(self.c)
        self.b = self.p.block_view("mainBlock")

        # check that do_initial_reset works asynchronously
        assert self.c.state.value == "Disabled"
        self.p.start()
        assert self.c.state.value == "Ready"
    def setUp(self):
        self.p = Process("process1")

        # create a child to client
        self.c_child = StatefulController("childBlock")
        self.c_part = MyPart("cp1")
        self.c_child.add_part(self.c_part)
        self.p.add_controller(self.c_child)

        # Create temporary config directory for ProcessController
        self.config_dir = tmp_dir("config_dir")
        self.main_block_name = "mainBlock"
        self.c = ManagerController("mainBlock",
                                   config_dir=self.config_dir.value)
        self.c.add_part(MyPart("part1"))
        self.c.add_part(
            ChildPart("part2", mri="childBlock", initial_visibility=True))
        self.p.add_controller(self.c)
        self.b = self.p.block_view("mainBlock")

        # check that do_initial_reset works asynchronously
        assert self.c.state.value == "Disabled"
        self.p.start()
        assert self.c.state.value == "Ready"
예제 #10
0
class TestStatefulController(unittest.TestCase):
    def setUp(self):
        self.process = Process("proc")
        self.params = Mock()
        self.params.mri = "MyMRI"
        self.params.description = "My description"
        self.part = MyPart("testpart")
        self.o = StatefulController(self.process, [self.part], self.params)
        self.process.add_controller(self.params.mri, self.o)

    def start_process(self):
        self.process.start()
        self.addCleanup(self.stop_process)

    def stop_process(self):
        if self.process.started:
            self.process.stop(timeout=1)

    def test_process_init(self, ):
        assert not hasattr(self.part, "started")
        self.start_process()
        assert self.part.started

    def test_process_stop(self):
        self.start_process()
        assert not hasattr(self.part, "halted")
        self.process.stop(timeout=1)
        assert self.part.halted

    def test_init(self):
        assert self.o.state.value == "Disabled"
        self.start_process()
        assert self.o.state.value == "Ready"

    def test_reset_fails_from_ready(self):
        self.start_process()
        with self.assertRaises(TypeError):
            self.o.reset()
        assert not hasattr(self.part, "reset_done")

    def test_disable(self):
        self.start_process()
        assert not hasattr(self.part, "disable_done")
        self.o.disable()
        assert self.part.disable_done
        assert self.o.state.value == "Disabled"
        assert not hasattr(self.part, "reset_done")
        self.o.reset()
        assert self.part.reset_done
        assert self.o.state.value == "Ready"
예제 #11
0
class TestStatefulController(unittest.TestCase):
    def setUp(self):
        self.process = Process("proc")
        self.part = MyPart("testpart")
        self.part2 = MyPart("testpart2")
        self.o = StatefulController("MyMRI")
        self.o.add_part(self.part)
        self.o.add_part(self.part2)
        self.process.add_controller(self.o)
        self.b = self.process.block_view("MyMRI")

    def start_process(self):
        self.process.start()
        self.addCleanup(self.stop_process)

    def stop_process(self):
        if self.process.state:
            self.process.stop(timeout=1)

    def test_process_init(self, ):
        assert not self.part.started
        self.start_process()
        assert self.part.started

    def test_process_stop(self):
        self.start_process()
        assert not self.part.halted
        self.process.stop(timeout=1)
        assert self.part.halted

    def test_init(self):
        assert self.b.state.value == "Disabled"
        self.start_process()
        assert list(self.b) == ['meta', 'health', 'state', 'disable', 'reset']
        assert self.b.state.value == "Ready"
        assert self.b.disable.writeable is True
        assert self.b.reset.writeable is False

    def test_reset_fails_from_ready(self):
        self.start_process()
        with self.assertRaises(TypeError):
            self.o.reset()
        assert not self.part.reset_done

    def test_disable(self):
        self.start_process()
        assert not self.part.disable_done
        self.b.disable()
        assert self.part.disable_done
        assert self.b.state.value == "Disabled"
        with self.assertRaises(NotWriteableError) as cm:
            self.b.disable()
        assert str(cm.exception) == \
            "Method ['MyMRI', 'disable'] is not writeable in state Disabled"
        assert not self.part.reset_done
        self.b.reset()
        assert self.part.reset_done
        assert self.b.state.value == "Ready"

    def test_run_hook(self):
        self.start_process()
        part_contexts = self.o.create_part_contexts()
        result = self.o.run_hooks(
            SaveHook(p, c) for p, c in part_contexts.items())
        assert list(result) == ["testpart", "testpart2"]
        assert result["testpart"] == dict(foo="bartestpart")
        assert result["testpart2"] == dict(foo="bartestpart2")
        # The part.context is a weakref, so compare on one of its strong
        # methods instead
        assert self.part.context.sleep == part_contexts[self.part].sleep
        del part_contexts
        gc.collect()
        with self.assertRaises(ReferenceError):
            self.part.context.sleep(0)

    def test_run_hook_raises(self):
        self.start_process()

        class MyException(Exception):
            pass

        self.part.exception = MyException()
        with self.assertRaises(Exception) as cm:
            self.o.run_hooks(
                SaveHook(p, c)
                for p, c in self.o.create_part_contexts().items())
        self.assertIs(self.part.context, None)
        self.assertIs(cm.exception, self.part.exception)
 def child_block():
     controllers, parts = adbase_parts(prefix="prefix")
     controller = StatefulController("WINDOWS:DETECTOR")
     for part in parts:
         controller.add_part(part)
     return controllers + [controller]
예제 #13
0
class TestManagerController(unittest.TestCase):
    maxDiff = None

    def setUp(self):
        self.p = Process('process1')

        # create a child to client
        self.c_child = StatefulController("childBlock")
        self.c_part = MyPart("cp1")
        self.c_child.add_part(self.c_part)
        self.p.add_controller(self.c_child)

        # create a root block for the ManagerController block to reside in
        if os.path.isdir("/tmp/mainBlock"):
            shutil.rmtree("/tmp/mainBlock")
        self.c = ManagerController('mainBlock', config_dir="/tmp")
        self.c.add_part(MyPart("part1"))
        self.c.add_part(
            ChildPart("part2", mri="childBlock", initial_visibility=True))
        self.p.add_controller(self.c)
        self.b = self.p.block_view("mainBlock")

        # check that do_initial_reset works asynchronously
        assert self.c.state.value == "Disabled"
        self.p.start()
        assert self.c.state.value == "Ready"

    def tearDown(self):
        self.p.stop(timeout=1)

    def test_init(self):
        assert self.c.layout.value.name == ["part2"]
        assert self.c.layout.value.mri == ["childBlock"]
        assert self.c.layout.value.x == [0.0]
        assert self.c.layout.value.y == [0.0]
        assert self.c.layout.value.visible == [True]
        assert self.c.layout.meta.elements["name"].writeable is False
        assert self.c.layout.meta.elements["mri"].writeable is False
        assert self.c.layout.meta.elements["x"].writeable is True
        assert self.c.layout.meta.elements["y"].writeable is True
        assert self.c.layout.meta.elements["visible"].writeable is True
        assert self.c.design.value == ""
        assert self.c.exports.value.source == []
        assert self.c.exports.meta.elements["source"].choices == \
               ['part2.health', 'part2.state', 'part2.disable', 'part2.reset',
                'part2.attr']
        assert self.c.exports.value.export == []
        assert self.c.modified.value is False
        assert self.c.modified.alarm.message == ""
        assert self.b.mri.value == "mainBlock"
        assert self.b.mri.meta.tags == ["sourcePort:block:mainBlock"]

    def check_expected_save(self,
                            x=0.0,
                            y=0.0,
                            visible="true",
                            attr="defaultv"):
        expected = [
            x.strip() for x in ("""{
          "attributes": {
             "layout": {
               "part2": {
                 "x": %s,
                 "y": %s,
                 "visible": %s
               }
             },
             "exports": {},
             "attr": "defaultv"
          },
          "children": {
             "part2": {
               "attr": "%s"
             }
          }
        }""" % (x, y, visible, attr)).splitlines()
        ]
        with open("/tmp/mainBlock/testSaveLayout.json") as f:
            actual = [x.strip() for x in f.readlines()]
        assert actual == expected

    def test_save(self):
        self.c._run_git_cmd = MagicMock()
        assert self.c.design.value == ""
        assert self.c.design.meta.choices == [""]
        c = Context(self.p)
        l = []
        c.subscribe(["mainBlock", "design", "meta"], l.append)
        # Wait for long enough for the other process to get a look in
        c.sleep(0.1)
        assert len(l) == 1
        assert l.pop()["choices"] == [""]
        b = c.block_view("mainBlock")
        b.save(design="testSaveLayout")
        assert len(l) == 3
        assert l[0]["writeable"] == False
        assert l[1]["choices"] == ["", "testSaveLayout"]
        assert l[2]["writeable"] == True
        assert self.c.design.meta.choices == ["", "testSaveLayout"]
        self.check_expected_save()
        assert self.c.state.value == "Ready"
        assert self.c.design.value == 'testSaveLayout'
        assert self.c.modified.value is False
        os.remove("/tmp/mainBlock/testSaveLayout.json")
        self.c_part.attr.set_value("newv")
        assert self.c.modified.value is True
        assert self.c.modified.alarm.message == \
               "part2.attr.value = 'newv' not 'defaultv'"
        self.c.save(design="")
        self.check_expected_save(attr="newv")
        assert self.c.design.value == 'testSaveLayout'
        assert self.c._run_git_cmd.call_args_list == [
            call('add', '/tmp/mainBlock/testSaveLayout.json'),
            call('commit', '--allow-empty', '-m',
                 'Saved mainBlock testSaveLayout',
                 '/tmp/mainBlock/testSaveLayout.json'),
            call('add', '/tmp/mainBlock/testSaveLayout.json'),
            call('commit', '--allow-empty', '-m',
                 'Saved mainBlock testSaveLayout',
                 '/tmp/mainBlock/testSaveLayout.json')
        ]

    def move_child_block(self):
        new_layout = dict(name=["part2"],
                          mri=["anything"],
                          x=[10],
                          y=[20],
                          visible=[True])
        self.b.layout.put_value(new_layout)

    def test_move_child_block_dict(self):
        assert self.b.layout.value.x == [0]
        self.move_child_block()
        assert self.b.layout.value.x == [10]

    def test_set_and_load_layout(self):
        new_layout = LayoutTable(name=["part2"],
                                 mri=["anything"],
                                 x=[10],
                                 y=[20],
                                 visible=[False])
        self.c.set_layout(new_layout)
        assert self.c.parts['part2'].x == 10
        assert self.c.parts['part2'].y == 20
        assert self.c.parts['part2'].visible == False
        assert self.c.modified.value == True
        assert self.c.modified.alarm.message == "layout changed"

        # save the layout, modify and restore it
        self.b.save(design='testSaveLayout')
        assert self.c.modified.value is False
        self.check_expected_save(10.0, 20.0, "false")
        self.c.parts['part2'].x = 30
        self.c.set_design('testSaveLayout')
        assert self.c.parts['part2'].x == 10

    def test_set_export_parts(self):
        context = Context(self.p)
        b = context.block_view("mainBlock")
        assert list(b) == [
            'meta', 'health', 'state', 'disable', 'reset', 'mri', 'layout',
            'design', 'exports', 'modified', 'save', 'attr'
        ]
        assert b.attr.meta.tags == ["widget:textinput"]
        new_exports = ExportTable.from_rows([('part2.attr', 'childAttr'),
                                             ('part2.reset', 'childReset')])
        self.c.set_exports(new_exports)
        assert self.c.modified.value == True
        assert self.c.modified.alarm.message == "exports changed"
        self.c.save(design='testSaveLayout')
        assert self.c.modified.value == False
        # block has changed, get a new view
        b = context.block_view("mainBlock")
        assert list(b) == [
            'meta', 'health', 'state', 'disable', 'reset', 'mri', 'layout',
            'design', 'exports', 'modified', 'save', 'attr', 'childAttr',
            'childReset'
        ]
        assert self.c.state.value == "Ready"
        assert b.childAttr.value == "defaultv"
        assert self.c.modified.value == False
        m = MagicMock()
        f = b.childAttr.subscribe_value(m)
        # allow a subscription to come through
        context.sleep(0.1)
        m.assert_called_once_with("defaultv")
        m.reset_mock()
        self.c_part.attr.set_value("newv")
        assert b.childAttr.value == "newv"
        assert self.c_part.attr.value == "newv"
        assert self.c.modified.value == True
        assert self.c.modified.alarm.message == \
               "part2.attr.value = 'newv' not 'defaultv'"
        # allow a subscription to come through
        context.sleep(0.1)
        m.assert_called_once_with("newv")
        b.childAttr.put_value("again")
        assert b.childAttr.value == "again"
        assert self.c_part.attr.value == "again"
        assert self.c.modified.value == True
        assert self.c.modified.alarm.message == \
               "part2.attr.value = 'again' not 'defaultv'"
        # remove the field
        new_exports = ExportTable([], [])
        self.c.set_exports(new_exports)
        assert self.c.modified.value == True
        self.c.save()
        assert self.c.modified.value == False
        # block has changed, get a new view
        b = context.block_view("mainBlock")
        assert "childAttr" not in b
예제 #14
0
 def create_block(self, p):
     c = StatefulController("mri")
     c.add_part(p)
     self.process.add_controller(c)
     b = self.process.block_view("mri")
     return b
예제 #15
0
class TestManagerController(unittest.TestCase):
    maxDiff = None

    def setUp(self):
        self.p = Process("process1")

        # create a child to client
        self.c_child = StatefulController("childBlock")
        self.c_part = MyPart("cp1")
        self.c_child.add_part(self.c_part)
        self.p.add_controller(self.c_child)

        # Create temporary config directory for ProcessController
        self.config_dir = tmp_dir("config_dir")
        self.main_block_name = "mainBlock"
        self.c = ManagerController("mainBlock",
                                   config_dir=self.config_dir.value)
        self.c.add_part(MyPart("part1"))
        self.c.add_part(
            ChildPart("part2", mri="childBlock", initial_visibility=True))
        self.p.add_controller(self.c)
        self.b = self.p.block_view("mainBlock")

        # check that do_initial_reset works asynchronously
        assert self.c.state.value == "Disabled"
        self.p.start()
        assert self.c.state.value == "Ready"

    def tearDown(self):
        self.p.stop(timeout=1)
        shutil.rmtree(self.config_dir.value)

    def test_init(self):
        assert self.c.layout.value.name == ["part2"]
        assert self.c.layout.value.mri == ["childBlock"]
        assert self.c.layout.value.x == [0.0]
        assert self.c.layout.value.y == [0.0]
        assert self.c.layout.value.visible == [True]
        assert self.c.layout.meta.elements["name"].writeable is False
        assert self.c.layout.meta.elements["mri"].writeable is False
        assert self.c.layout.meta.elements["x"].writeable is True
        assert self.c.layout.meta.elements["y"].writeable is True
        assert self.c.layout.meta.elements["visible"].writeable is True
        assert self.c.design.value == ""
        assert self.c.exports.value.source == []
        assert self.c.exports.meta.elements["source"].choices == [
            "part2.health",
            "part2.state",
            "part2.disable",
            "part2.reset",
            "part2.attr",
        ]
        assert self.c.exports.value.export == []
        assert self.c.modified.value is False
        assert self.c.modified.alarm.message == ""
        assert self.b.mri.value == "mainBlock"
        assert self.b.mri.meta.tags == ["sourcePort:block:mainBlock"]

    def _get_design_filename(self, block_name, design_name):
        return f"{self.config_dir.value}/{block_name}/{design_name}.json"

    def check_expected_save(self,
                            design_name,
                            x=0.0,
                            y=0.0,
                            visible="true",
                            attr="defaultv"):
        expected = [
            x.strip() for x in ("""{
          "attributes": {
             "layout": {
               "part2": {
                 "x": %s,
                 "y": %s,
                 "visible": %s
               }
             },
             "exports": {},
             "attr": "defaultv"
          },
          "children": {
             "part2": {
               "attr": "%s"
             }
          }
        }""" % (x, y, visible, attr)).splitlines()
        ]
        with open(self._get_design_filename(self.main_block_name,
                                            design_name)) as f:
            actual = [x.strip() for x in f.readlines()]
        assert actual == expected

    def test_save(self):
        self.c._run_git_cmd = MagicMock()
        assert self.c.design.value == ""
        assert self.c.design.meta.choices == [""]
        c = Context(self.p)
        li = []
        c.subscribe(["mainBlock", "design", "meta"], li.append)
        # Wait for long enough for the other process to get a look in
        c.sleep(0.1)
        assert len(li) == 1
        assert li.pop()["choices"] == [""]
        b = c.block_view("mainBlock")
        design_name = "testSaveLayout"
        b.save(designName=design_name)
        assert len(li) == 3
        assert li[0]["writeable"] is False
        assert li[1]["choices"] == ["", design_name]
        assert li[2]["writeable"] is True
        assert self.c.design.meta.choices == ["", design_name]
        self.check_expected_save(design_name)
        assert self.c.state.value == "Ready"
        assert self.c.design.value == design_name
        assert self.c.modified.value is False
        os.remove(self._get_design_filename(self.main_block_name, design_name))
        self.c_part.attr.set_value("newv")
        assert self.c.modified.value is True
        assert (self.c.modified.alarm.message ==
                "part2.attr.value = 'newv' not 'defaultv'")
        self.c.save(designName="")
        self.check_expected_save(design_name, attr="newv")
        design_filename = self._get_design_filename(self.main_block_name,
                                                    design_name)
        assert self.c.design.value == "testSaveLayout"
        assert self.c._run_git_cmd.call_args_list == [
            call("add", design_filename),
            call(
                "commit",
                "--allow-empty",
                "-m",
                "Saved mainBlock testSaveLayout",
                design_filename,
            ),
            call("add", design_filename),
            call(
                "commit",
                "--allow-empty",
                "-m",
                "Saved mainBlock testSaveLayout",
                design_filename,
            ),
        ]

    def move_child_block(self):
        new_layout = dict(name=["part2"],
                          mri=["anything"],
                          x=[10],
                          y=[20],
                          visible=[True])
        self.b.layout.put_value(new_layout)

    def test_move_child_block_dict(self):
        assert self.b.layout.value.x == [0]
        self.move_child_block()
        assert self.b.layout.value.x == [10]

    def test_set_and_load_layout(self):
        new_layout = LayoutTable(name=["part2"],
                                 mri=["anything"],
                                 x=[10],
                                 y=[20],
                                 visible=[False])
        self.c.set_layout(new_layout)
        assert self.c.parts["part2"].x == 10
        assert self.c.parts["part2"].y == 20
        assert self.c.parts["part2"].visible is False
        assert self.c.modified.value is True
        assert self.c.modified.alarm.message == "layout changed"

        # save the layout, modify and restore it
        design_name = "testSaveLayout"
        self.b.save(designName=design_name)
        assert self.c.modified.value is False
        self.check_expected_save(design_name, 10.0, 20.0, "false")
        self.c.parts["part2"].x = 30
        self.c.set_design(design_name)
        assert self.c.parts["part2"].x == 10

    def test_set_export_parts(self):
        context = Context(self.p)
        b = context.block_view("mainBlock")
        assert list(b) == [
            "meta",
            "health",
            "state",
            "disable",
            "reset",
            "mri",
            "layout",
            "design",
            "exports",
            "modified",
            "save",
            "attr",
        ]
        assert b.attr.meta.tags == ["widget:textinput"]
        new_exports = ExportTable.from_rows([("part2.attr", "childAttr"),
                                             ("part2.reset", "childReset")])
        self.c.set_exports(new_exports)
        assert self.c.modified.value is True
        assert self.c.modified.alarm.message == "exports changed"
        self.c.save(designName="testSaveLayout")
        assert self.c.modified.value is False
        # block has changed, get a new view
        b = context.block_view("mainBlock")
        assert list(b) == [
            "meta",
            "health",
            "state",
            "disable",
            "reset",
            "mri",
            "layout",
            "design",
            "exports",
            "modified",
            "save",
            "attr",
            "childAttr",
            "childReset",
        ]
        assert self.c.state.value == "Ready"
        assert b.childAttr.value == "defaultv"
        assert self.c.modified.value is False
        m = MagicMock()
        b.childAttr.subscribe_value(m)
        # allow a subscription to come through
        context.sleep(0.1)
        m.assert_called_once_with("defaultv")
        m.reset_mock()
        self.c_part.attr.set_value("newv")
        assert b.childAttr.value == "newv"
        assert self.c_part.attr.value == "newv"
        assert self.c.modified.value is True
        assert (self.c.modified.alarm.message ==
                "part2.attr.value = 'newv' not 'defaultv'")
        # allow a subscription to come through
        context.sleep(0.1)
        m.assert_called_once_with("newv")
        b.childAttr.put_value("again")
        assert b.childAttr.value == "again"
        assert self.c_part.attr.value == "again"
        assert self.c.modified.value is True
        assert (self.c.modified.alarm.message ==
                "part2.attr.value = 'again' not 'defaultv'")
        # remove the field
        new_exports = ExportTable([], [])
        self.c.set_exports(new_exports)
        assert self.c.modified.value is True
        self.c.save()
        assert self.c.modified.value is False
        # block has changed, get a new view
        b = context.block_view("mainBlock")
        assert "childAttr" not in b