1# Copyright 2017 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5 6import contextlib 7import logging 8import os 9import tempfile 10 11from devil.utils import reraiser_thread 12 13 14class Datatype: 15 HTML = 'text/html' 16 JSON = 'application/json' 17 PNG = 'image/png' 18 TEXT = 'text/plain' 19 20 21class OutputManager: 22 23 def __init__(self): 24 """OutputManager Constructor. 25 26 This class provides a simple interface to save test output. Subclasses 27 of this will allow users to save test results in the cloud or locally. 28 """ 29 self._allow_upload = False 30 self._thread_group = None 31 32 @contextlib.contextmanager 33 def ArchivedTempfile( 34 self, out_filename, out_subdir, datatype=Datatype.TEXT): 35 """Archive file contents asynchonously and then deletes file. 36 37 Args: 38 out_filename: Name for saved file. 39 out_subdir: Directory to save |out_filename| to. 40 datatype: Datatype of file. 41 42 Returns: 43 An ArchivedFile file. This file will be uploaded async when the context 44 manager exits. AFTER the context manager exits, you can get the link to 45 where the file will be stored using the Link() API. You can use typical 46 file APIs to write and flish the ArchivedFile. You can also use file.name 47 to get the local filepath to where the underlying file exists. If you do 48 this, you are responsible of flushing the file before exiting the context 49 manager. 50 """ 51 if not self._allow_upload: 52 raise Exception('Must run |SetUp| before attempting to upload!') 53 54 f = self.CreateArchivedFile(out_filename, out_subdir, datatype) 55 try: 56 yield f 57 finally: 58 self.ArchiveArchivedFile(f, delete=True) 59 60 def CreateArchivedFile(self, out_filename, out_subdir, 61 datatype=Datatype.TEXT): 62 """Returns an instance of ArchivedFile.""" 63 return self._CreateArchivedFile(out_filename, out_subdir, datatype) 64 65 def _CreateArchivedFile(self, out_filename, out_subdir, datatype): 66 raise NotImplementedError 67 68 def ArchiveArchivedFile(self, archived_file, delete=False): 69 """Archive an ArchivedFile instance and optionally delete it.""" 70 if not isinstance(archived_file, ArchivedFile): 71 raise Exception('Excepting an instance of ArchivedFile, got %s.' % 72 type(archived_file)) 73 archived_file.PrepareArchive() 74 75 def archive(): 76 try: 77 archived_file.Archive() 78 finally: 79 if delete: 80 archived_file.Delete() 81 82 thread = reraiser_thread.ReraiserThread(func=archive) 83 thread.start() 84 self._thread_group.Add(thread) 85 86 def SetUp(self): 87 self._allow_upload = True 88 self._thread_group = reraiser_thread.ReraiserThreadGroup() 89 90 def TearDown(self): 91 self._allow_upload = False 92 logging.info('Finishing archiving output.') 93 self._thread_group.JoinAll() 94 95 def __enter__(self): 96 self.SetUp() 97 return self 98 99 def __exit__(self, _exc_type, _exc_val, _exc_tb): 100 self.TearDown() 101 102 103class ArchivedFile: 104 105 def __init__(self, out_filename, out_subdir, datatype): 106 self._out_filename = out_filename 107 self._out_subdir = out_subdir 108 self._datatype = datatype 109 110 mode = 'w+' 111 if datatype == Datatype.PNG: 112 mode = 'w+b' 113 self._f = tempfile.NamedTemporaryFile(mode=mode, delete=False) 114 self._ready_to_archive = False 115 116 @property 117 def name(self): 118 return self._f.name 119 120 def fileno(self, *args, **kwargs): 121 if self._ready_to_archive: 122 raise Exception('Cannot retrieve the integer file descriptor ' 123 'after archiving has begun!') 124 return self._f.fileno(*args, **kwargs) 125 126 def write(self, *args, **kwargs): 127 if self._ready_to_archive: 128 raise Exception('Cannot write to file after archiving has begun!') 129 self._f.write(*args, **kwargs) 130 131 def flush(self, *args, **kwargs): 132 if self._ready_to_archive: 133 raise Exception('Cannot flush file after archiving has begun!') 134 self._f.flush(*args, **kwargs) 135 136 def Link(self): 137 """Returns location of archived file.""" 138 if not self._ready_to_archive: 139 raise Exception('Cannot get link to archived file before archiving ' 140 'has begun') 141 return self._Link() 142 143 def _Link(self): 144 """Note for when overriding this function. 145 146 This function will certainly be called before the file 147 has finished being archived. Therefore, this needs to be able to know the 148 exact location of the archived file before it is finished being archived. 149 """ 150 raise NotImplementedError 151 152 def PrepareArchive(self): 153 """Meant to be called synchronously to prepare file for async archiving.""" 154 self.flush() 155 self._ready_to_archive = True 156 self._PrepareArchive() 157 158 def _PrepareArchive(self): 159 """Note for when overriding this function. 160 161 This function is needed for things such as computing the location of 162 content addressed files. This is called after the file is written but 163 before archiving has begun. 164 """ 165 166 def Archive(self): 167 """Archives file.""" 168 if not self._ready_to_archive: 169 raise Exception('File is not ready to archive. Be sure you are not ' 170 'writing to the file and PrepareArchive has been called') 171 self._Archive() 172 173 def _Archive(self): 174 raise NotImplementedError 175 176 def Delete(self): 177 """Deletes the backing file.""" 178 self._f.close() 179 os.remove(self.name) 180