• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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