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 28, 2019
1 parent 29fe78e commit c6cfaaf
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 13 deletions.
21 changes: 18 additions & 3 deletions stestr/repository/abstract.py
Original file line number Diff line number Diff line change
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 @@ -83,9 +83,9 @@ def get_inserter(self, partial=False, run_id=None):
that testtools 0.9.2 and above offer. The startTestRun and
stopTestRun methods in particular must be called.
"""
return self._get_inserter(partial, run_id)
return self._get_inserter(partial, run_id, metadata)

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
42 changes: 37 additions & 5 deletions stestr/repository/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,15 @@ 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:
print([x for x in db])
metadata = db.get(str(run_id))
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,10 +133,11 @@ 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)
def _get_inserter(self, partial, run_id=None, metadata=None):
return _Inserter(self, partial, run_id, metadata=metadata)

def _get_test_times(self, test_ids):
# May be too slow, but build and iterate.
Expand Down Expand Up @@ -167,15 +177,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.get(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 +233,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[str(run_id)] = self._metadata
finally:
db.close()
self.partial = partial
# The time take by each test, flushed at the end.
self._times = {}
Expand Down
5 changes: 3 additions & 2 deletions stestr/repository/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def latest_id(self):
raise KeyError("No tests in repository")
return result

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

def _get_test_times(self, test_ids):
Expand Down 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
Original file line number Diff line number Diff line change
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_metadata(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
20 changes: 20 additions & 0 deletions stestr/tests/repository/test_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,23 @@ def test_get_test_run_unexpected_ioerror_errno(self):
self.assertTrue(os.path.isfile(os.path.join(repo.base, '0')))
os.chmod(os.path.join(repo.base, '0'), 0000)
self.assertRaises(IOError, repo.get_test_run, '0')

def test_get_metadata(self):
repo = self.useFixture(FileRepositoryFixture()).repo
result = repo.get_inserter(metadata='fun')
result.startTestRun()
result.stopTestRun()
run = repo.get_test_run(result.get_id())
self.assertEqual('fun', run.get_metadata())

def test_find_metadata(self):
repo = self.useFixture(FileRepositoryFixture()).repo
result = repo.get_inserter(metadata='fun')
result.startTestRun()
result.stopTestRun()
result_bad = repo.get_inserter(metadata='not_fun')
result_bad.startTestRun()
result_bad.stopTestRun()
run_ids = repo.find_metadata('fun')
self.assertIn(result.get_id(), run_ids)
self.assertNotIn(bad_result.get_id(), run_ids)
20 changes: 20 additions & 0 deletions stestr/tests/repository/test_sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,23 @@ def test_run_get_subunit_stream(self):
self.assertIsNotNone(stream)
self.assertTrue(stream.readable())
self.assertEqual([], stream.readlines())

def test_get_metadata(self):
repo = self.useFixture(SqlRepositoryFixture(url=self.url)).repo
result = repo.get_inserter(metadata='fun')
result.startTestRun()
result.stopTestRun()
run = repo.get_test_run(result.get_id())
self.assertEqual('fun', run.get_metadata())

def test_find_metadata(self):
repo = self.useFixture(SqlRepositoryFixture(url=self.url)).repo
result = repo.get_inserter(metadata='fun')
result.startTestRun()
result.stopTestRun()
result_bad = repo.get_inserter(metadata='not_fun')
result_bad.startTestRun()
result_bad.stopTestRun()
run_ids = repo.find_metadata('fun')
self.assertIn(result.get_id(), run_ids)
self.assertNotIn(result_bad.get_id(), run_ids)

0 comments on commit c6cfaaf

Please sign in to comment.