Skip to content

Commit

Permalink
Add support to repositories for storing run metadata
Browse files Browse the repository at this point in the history
This commit adds repository APIs around storing and accessing run
metadata strings. The concept being that you can store a metadata string
to identify a run by some other identifier then the run id. The example
use case in mind for this feature is using it to store a VCS commit hash
(like a git sha1) or identifier. The APIs in this commit allow storing
metadata at run creation time, accessing the metadata for a run, and
finding run_ids by a metadata value. This is implemented for both the
file and sql repository types. The memory repository type does not have
this implemented, as it is more limited.

Related to: #224
  • Loading branch information
mtreinish committed Feb 27, 2019
1 parent 29fe78e commit 92b4951
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 9 deletions.
19 changes: 17 additions & 2 deletions stestr/repository/abstract.py
Expand Up @@ -67,7 +67,7 @@ def get_failing(self):
"""
raise NotImplementedError(self.get_failing)

def get_inserter(self, partial=False, run_id=None):
def get_inserter(self, partial=False, run_id=None, metadata=None):
"""Get an inserter that will insert a test run into the repository.
Repository implementations should implement _get_inserter.
Expand All @@ -85,7 +85,7 @@ def get_inserter(self, partial=False, run_id=None):
"""
return self._get_inserter(partial, run_id)

def _get_inserter(self, partial=False, run_id=None):
def _get_inserter(self, partial=False, run_id=None, metadata=None):
"""Get an inserter for get_inserter.
The result is decorated with an AutoTimingTestResultDecorator.
Expand Down Expand Up @@ -156,6 +156,14 @@ def gather(test_dict):
result.stopTestRun()
return ids

def find_metadata(self, metadata):
"""Return the list of run_ids for a given metadata string.
:param: metadata: the metadata string to search for.
:return: a list of any test_ids that have that metadata value.
"""
raise NotImplementedError(self.find_metadata)


class AbstractTestRun(object):
"""A test run that has been stored in a repository.
Expand Down Expand Up @@ -186,6 +194,13 @@ def get_test(self):
"""
raise NotImplementedError(self.get_test)

def get_metadata(self):
"""Get the metadata value for the test run.
:return: A string of the metadata or None if it doesn't exist.
"""
raise NotImplementedError(self.get_metadata)


class RepositoryNotFound(Exception):
"""Raised when we try to open a repository that isn't there."""
Expand Down
40 changes: 37 additions & 3 deletions stestr/repository/file.py
Expand Up @@ -115,6 +115,17 @@ def get_failing(self):
raise
return _DiskRun(None, run_subunit_content)

def _get_metadata(self, run_id):
db = my_dbm.open(self._path('meta.dbm'), 'c')
try:
try:
metadata = db[run_id]
except KeyError:
metadata = None
finally:
db.close()
return metadata

def get_test_run(self, run_id):
try:
with open(os.path.join(self.base, str(run_id)), 'rb') as fp:
Expand All @@ -124,7 +135,8 @@ def get_test_run(self, run_id):
raise KeyError("No such run.")
else:
raise
return _DiskRun(run_id, run_subunit_content)
metadata = self._get_metadata(run_id)
return _DiskRun(run_id, run_subunit_content, metadata=metadata)

def _get_inserter(self, partial, run_id=None):
return _Inserter(self, partial, run_id)
Expand Down Expand Up @@ -167,15 +179,27 @@ def _write_next_stream(self, value):
stream.write('%d\n' % value)
atomicish_rename(prefix + '.new', prefix)

def find_metadata(self, metadata):
run_ids = []
db = my_dbm.open(self._path('meta.dbm'), 'c')
try:
for run_id in db:
if db[run_id] == metadata:
run_ids.append(run_id)
finally:
db.close()
return run_ids


class _DiskRun(repository.AbstractTestRun):
"""A test run that was inserted into the repository."""

def __init__(self, run_id, subunit_content):
def __init__(self, run_id, subunit_content, metadata=None):
"""Create a _DiskRun with the content subunit_content."""
self._run_id = run_id
self._content = subunit_content
assert type(subunit_content) is bytes
self._metadata = metadata

def get_id(self):
return self._run_id
Expand Down Expand Up @@ -211,21 +235,31 @@ def wrap_result(result):
case, wrap_result, methodcaller('startTestRun'),
methodcaller('stopTestRun'))

def get_metadata(self):
return self._metadata


class _SafeInserter(object):

def __init__(self, repository, partial=False, run_id=None):
def __init__(self, repository, partial=False, run_id=None, metadata=None):
# XXX: Perhaps should factor into a decorator and use an unaltered
# TestProtocolClient.
self._repository = repository
self._run_id = run_id
self._metadata = metadata
if not self._run_id:
fd, name = tempfile.mkstemp(dir=self._repository.base)
self.fname = name
stream = os.fdopen(fd, 'wb')
else:
self.fname = os.path.join(self._repository.base, self._run_id)
stream = open(self.fname, 'ab')
if self._metadata:
db = my_dbm.open(self._repository._path('meta.dbm'), 'c')
try:
db[self._run_id] = self._metadata
finally:
db.close()
self.partial = partial
# The time take by each test, flushed at the end.
self._times = {}
Expand Down
3 changes: 2 additions & 1 deletion stestr/repository/memory.py
Expand Up @@ -127,13 +127,14 @@ def run(self, result):
class _Inserter(repository.AbstractTestRun):
"""Insert test results into a memory repository."""

def __init__(self, repository, partial, run_id=None):
def __init__(self, repository, partial, run_id=None, metadata=None):
self._repository = repository
self._partial = partial
self._tests = []
# Subunit V2 stream for get_subunit_stream
self._subunit = None
self._run_id = run_id
self._metadata = metadata

def startTestRun(self):
self._subunit = BytesIO()
Expand Down
25 changes: 22 additions & 3 deletions stestr/repository/sql.py
Expand Up @@ -116,8 +116,8 @@ def get_failing(self):
def get_test_run(self, run_id):
return _Subunit2SqlRun(self.base, run_id)

def _get_inserter(self, partial, run_id=None):
return _SqlInserter(self, partial, run_id)
def _get_inserter(self, partial, run_id=None, metadata=None):
return _SqlInserter(self, partial, run_id, metadata)

def _get_test_times(self, test_ids):
result = {}
Expand All @@ -136,6 +136,12 @@ def _get_test_times(self, test_ids):
session.close()
return result

def find_metadata(self, metadata):
session = self.session_factory()
runs = db_api.get_runs_by_key_value('stestr_run_meta', metadata,
session=session)
return [x.uuid for x in runs]


class _Subunit2SqlRun(repository.AbstractTestRun):
"""A test run that was inserted into the repository."""
Expand Down Expand Up @@ -177,15 +183,25 @@ def get_test(self):
case = subunit.ByteStreamToStreamResult(stream)
return case

def get_metdata(self):
if self._run_id:
session = self.session_factory()
metadata = db_api.get_run_metadata(self._run_id, session=session)
for meta in metadata:
if meta.key == 'stestr_run_meta':
return meta.value
return None


class _SqlInserter(repository.AbstractTestRun):
"""Insert test results into a sql repository."""

def __init__(self, repository, partial=False, run_id=None):
def __init__(self, repository, partial=False, run_id=None, metadata=None):
self._repository = repository
self.partial = partial
self._subunit = None
self._run_id = run_id
self._metadata = metadata
# Create a new session factory
self.engine = sqlalchemy.create_engine(self._repository.base)
self.session_factory = orm.sessionmaker(bind=self.engine,
Expand All @@ -202,6 +218,9 @@ def startTestRun(self):
session = self.session_factory()
if not self._run_id:
self.run = db_api.create_run(session=session)
if self._metadata:
db_api.add_run_metadata({'stestr_run_meta': self._metadata},
self.run.id, session=session)
self._run_id = self.run.uuid
else:
int_id = db_api.get_run_id_from_uuid(self._run_id, session=session)
Expand Down

0 comments on commit 92b4951

Please sign in to comment.