| # Copyright 2017 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from __future__ import absolute_import |
| import contextlib |
| import logging |
| import os |
| import tempfile |
| |
| from devil.utils import reraiser_thread |
| |
| |
| class Datatype(object): |
| HTML = 'text/html' |
| JSON = 'application/json' |
| PNG = 'image/png' |
| TEXT = 'text/plain' |
| |
| |
| class OutputManager(object): |
| |
| def __init__(self): |
| """OutputManager Constructor. |
| |
| This class provides a simple interface to save test output. Subclasses |
| of this will allow users to save test results in the cloud or locally. |
| """ |
| self._allow_upload = False |
| self._thread_group = None |
| |
| @contextlib.contextmanager |
| def ArchivedTempfile( |
| self, out_filename, out_subdir, datatype=Datatype.TEXT): |
| """Archive file contents asynchonously and then deletes file. |
| |
| Args: |
| out_filename: Name for saved file. |
| out_subdir: Directory to save |out_filename| to. |
| datatype: Datatype of file. |
| |
| Returns: |
| An ArchivedFile file. This file will be uploaded async when the context |
| manager exits. AFTER the context manager exits, you can get the link to |
| where the file will be stored using the Link() API. You can use typical |
| file APIs to write and flish the ArchivedFile. You can also use file.name |
| to get the local filepath to where the underlying file exists. If you do |
| this, you are responsible of flushing the file before exiting the context |
| manager. |
| """ |
| if not self._allow_upload: |
| raise Exception('Must run |SetUp| before attempting to upload!') |
| |
| f = self._CreateArchivedFile(out_filename, out_subdir, datatype) |
| try: |
| yield f |
| finally: |
| f.PrepareArchive() |
| |
| def archive(): |
| try: |
| f.Archive() |
| finally: |
| f.Delete() |
| |
| thread = reraiser_thread.ReraiserThread(func=archive) |
| thread.start() |
| self._thread_group.Add(thread) |
| |
| def _CreateArchivedFile(self, out_filename, out_subdir, datatype): |
| """Returns an instance of ArchivedFile.""" |
| raise NotImplementedError |
| |
| def SetUp(self): |
| self._allow_upload = True |
| self._thread_group = reraiser_thread.ReraiserThreadGroup() |
| |
| def TearDown(self): |
| self._allow_upload = False |
| logging.info('Finishing archiving output.') |
| self._thread_group.JoinAll() |
| |
| def __enter__(self): |
| self.SetUp() |
| return self |
| |
| def __exit__(self, _exc_type, _exc_val, _exc_tb): |
| self.TearDown() |
| |
| |
| class ArchivedFile(object): |
| |
| def __init__(self, out_filename, out_subdir, datatype): |
| self._out_filename = out_filename |
| self._out_subdir = out_subdir |
| self._datatype = datatype |
| |
| self._f = tempfile.NamedTemporaryFile(delete=False) |
| self._ready_to_archive = False |
| |
| @property |
| def name(self): |
| return self._f.name |
| |
| def write(self, *args, **kwargs): |
| if self._ready_to_archive: |
| raise Exception('Cannot write to file after archiving has begun!') |
| self._f.write(*args, **kwargs) |
| |
| def flush(self, *args, **kwargs): |
| if self._ready_to_archive: |
| raise Exception('Cannot flush file after archiving has begun!') |
| self._f.flush(*args, **kwargs) |
| |
| def Link(self): |
| """Returns location of archived file.""" |
| if not self._ready_to_archive: |
| raise Exception('Cannot get link to archived file before archiving ' |
| 'has begun') |
| return self._Link() |
| |
| def _Link(self): |
| """Note for when overriding this function. |
| |
| This function will certainly be called before the file |
| has finished being archived. Therefore, this needs to be able to know the |
| exact location of the archived file before it is finished being archived. |
| """ |
| raise NotImplementedError |
| |
| def PrepareArchive(self): |
| """Meant to be called synchronously to prepare file for async archiving.""" |
| self.flush() |
| self._ready_to_archive = True |
| self._PrepareArchive() |
| |
| def _PrepareArchive(self): |
| """Note for when overriding this function. |
| |
| This function is needed for things such as computing the location of |
| content addressed files. This is called after the file is written but |
| before archiving has begun. |
| """ |
| pass |
| |
| def Archive(self): |
| """Archives file.""" |
| if not self._ready_to_archive: |
| raise Exception('File is not ready to archive. Be sure you are not ' |
| 'writing to the file and PrepareArchive has been called') |
| self._Archive() |
| |
| def _Archive(self): |
| raise NotImplementedError |
| |
| def Delete(self): |
| """Deletes the backing file.""" |
| self._f.close() |
| os.remove(self.name) |