Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow users to reset the list of created records #114

Merged
merged 3 commits into from
Oct 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Unreleased_
-----------

- `Allow "status" and "severity" on In record init <../../pull/111>`_
- `Allow users to reset the list of created records <../../pull/114>`_

4.1.0_ - 2022-08-05
-------------------
Expand Down
6 changes: 6 additions & 0 deletions docs/reference/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,12 @@ The following attributes allow more direct access to record creation.

See `../how-to/use-soft-records` for a full example of its usage.

.. function:: ClearRecords()
This can be used to remove all created records. This means the same record
names can be re-used for different record types. This cannot be used once the
record database has been loaded, and is only designed to be used to help with
testing.

Finally, the following function is used to load record definitions before
starting the IOC.

Expand Down
14 changes: 10 additions & 4 deletions softioc/builder.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import numpy

from .device_core import RecordLookup
from .softioc import dbLoadDatabase

from epicsdbbuilder import *
Expand Down Expand Up @@ -273,9 +274,6 @@ def longStringOut(name, **fields):
# ----------------------------------------------------------------------------
# Support routines for builder


_DatabaseWritten = False

def LoadDatabase():
'''This should be called after all the builder records have been created,
but before calling iocInit(). The database is loaded into EPICS memory,
Expand All @@ -290,6 +288,14 @@ def LoadDatabase():
pythonSoftIoc.RecordWrapper.reset_builder()


def ClearRecords():
"""Delete all created record information, allowing new record creation"""
assert not pythonSoftIoc.RecordWrapper.is_builder_reset(), \
'Record database has already been loaded'
RecordLookup._RecordDirectory.clear()
ResetRecords()


# ----------------------------------------------------------------------------
# Record name configuration. A device name prefix must be specified.

Expand Down Expand Up @@ -317,7 +323,7 @@ def UnsetDevice():
'longStringIn', 'longStringOut',
'Action',
# Other builder support functions
'LoadDatabase',
'LoadDatabase', 'ClearRecords',
'SetDeviceName', 'UnsetDevice',
# Device support functions
'SetBlocking'
Expand Down
8 changes: 7 additions & 1 deletion softioc/pythonSoftIoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class RecordWrapper(object):
__builder_reset = False

def __init__(self, builder, device, name, **fields):
assert not self.__builder_reset, \
assert not self.is_builder_reset(), \
'It\'s too late to create records'
# List of keyword arguments expected by the device constructor. The
# remaining arguments are passed to the builder. It's a shame we
Expand Down Expand Up @@ -47,6 +47,12 @@ def reset_builder(cls):
for instance in cls.__Instances:
instance.__set('__builder', None)

@classmethod
def is_builder_reset(cls):
'''Returns True if it is too late to create records'''
return cls.__builder_reset



# Most attributes delegate directly to the builder instance until the
# database has been written. At this point the builder instance has been
Expand Down
2 changes: 0 additions & 2 deletions softioc/softioc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
from ctypes import *
from tempfile import NamedTemporaryFile

from epicsdbbuilder.recordset import recordset

from . import imports, device
from . import cothread_dispatcher

Expand Down
15 changes: 3 additions & 12 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
import pytest

# Must import softioc before epicsdbbuilder
from softioc.device_core import RecordLookup
from epicsdbbuilder import ResetRecords
from softioc.builder import ClearRecords

requires_cothread = pytest.mark.skipif(
sys.platform.startswith("win"), reason="Cothread doesn't work on windows"
Expand Down Expand Up @@ -88,20 +87,12 @@ def asyncio_ioc_override():
ioc.kill()
aioca_cleanup()

def _clear_records():
# Remove any records created at epicsdbbuilder layer
ResetRecords()
# And at pythonSoftIoc level
# TODO: Remove this hack and use use whatever comes out of
# https://github.com/dls-controls/pythonSoftIOC/issues/56
RecordLookup._RecordDirectory.clear()

@pytest.fixture(autouse=True)
def clear_records():
"""Deletes all records before and after every test"""
_clear_records()
ClearRecords()
yield
_clear_records()
ClearRecords()

@pytest.fixture(autouse=True)
def enable_code_coverage():
Expand Down
65 changes: 63 additions & 2 deletions tests/test_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
log,
create_random_prefix,
requires_cothread,
_clear_records,
WAVEFORM_LENGTH,
TIMEOUT,
select_and_recv,
Expand All @@ -19,6 +18,7 @@
from softioc import asyncio_dispatcher, builder, softioc
from softioc import alarm
from softioc.device import SetBlocking
from softioc.device_core import LookupRecord, LookupRecordList

# Test file for miscellaneous tests related to records

Expand All @@ -38,7 +38,6 @@ def test_records(tmp_path):
# Ensure we definitely unload all records that may be hanging over from
# previous tests, then create exactly one instance of expected records.
from sim_records import create_records
_clear_records()
create_records()

path = str(tmp_path / "records.db")
Expand Down Expand Up @@ -228,6 +227,68 @@ def test_setting_alarm_invalid_keywords(creation_func):

assert e.value.args[0] == 'Can\'t specify both status and STAT'

def test_clear_records():
"""Test that clearing the known records allows creation of a record of the
same name afterwards"""

device_name = create_random_prefix()
builder.SetDeviceName(device_name)

record_name = "NEW-RECORD"

builder.aOut(record_name)

# First check that we cannot create two of the same name
with pytest.raises(AssertionError):
builder.aOut(record_name)

builder.ClearRecords()

# Then confirm we can create one of this name
builder.aOut(record_name)

# Finally prove that the underlying record holder contains only one record
# Records are stored as full device:record name
full_name = device_name + ":" + record_name
assert LookupRecord(full_name)
assert len(LookupRecordList()) == 1
for key, val in LookupRecordList():
assert key == full_name

def clear_records_runner(child_conn):
"""Tests ClearRecords after loading the database"""
builder.aOut("Some-Record")
builder.LoadDatabase()

try:
builder.ClearRecords()
except AssertionError:
# Expected error
child_conn.send("D") # "Done"
child_conn.send("F") # "Fail"

def test_clear_records_too_late():
"""Test that calling ClearRecords after a database has been loaded raises
an exception"""
ctx = get_multiprocessing_context()

parent_conn, child_conn = ctx.Pipe()

process = ctx.Process(
target=clear_records_runner,
args=(child_conn,),
)

process.start()

try:
select_and_recv(parent_conn, "D")
finally:
process.join(timeout=TIMEOUT)
if process.exitcode is None:
pytest.fail("Process did not terminate")



def validate_fixture_names(params):
"""Provide nice names for the out_records fixture in TestValidate class"""
Expand Down