Commit ee7624c4 authored by dpranke@chromium.org's avatar dpranke@chromium.org

2011-04-08 Dirk Pranke <dpranke@chromium.org>

        Reviewed by Tony Chang.

        new-run-webkit-tests: implement support for audio tests.

        https://bugs.webkit.org/show_bug.cgi?id=57987

        * Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py:
        * Scripts/webkitpy/layout_tests/layout_package/single_test_runner.py:
        * Scripts/webkitpy/layout_tests/layout_package/test_expectations.py:
        * Scripts/webkitpy/layout_tests/layout_package/test_failures.py:
        * Scripts/webkitpy/layout_tests/layout_package/test_result_writer.py:
        * Scripts/webkitpy/layout_tests/port/base.py:
        * Scripts/webkitpy/layout_tests/port/test.py:
        * Scripts/webkitpy/layout_tests/port/mock_drt.py:
        * Scripts/webkitpy/layout_tests/port/webkit.py:

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@83330 268f45cc-cd09-0410-ab3c-d52691b4dbfc
parent ba1ac0c8
2011-04-08 Dirk Pranke <dpranke@chromium.org>
Reviewed by Tony Chang.
new-run-webkit-tests: implement support for audio tests.
https://bugs.webkit.org/show_bug.cgi?id=57987
* Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py:
* Scripts/webkitpy/layout_tests/layout_package/single_test_runner.py:
* Scripts/webkitpy/layout_tests/layout_package/test_expectations.py:
* Scripts/webkitpy/layout_tests/layout_package/test_failures.py:
* Scripts/webkitpy/layout_tests/layout_package/test_result_writer.py:
* Scripts/webkitpy/layout_tests/port/base.py:
* Scripts/webkitpy/layout_tests/port/test.py:
* Scripts/webkitpy/layout_tests/port/mock_drt.py:
* Scripts/webkitpy/layout_tests/port/webkit.py:
2011-04-08 Dirk Pranke <dpranke@chromium.org>
Reviewed by Ojan Vafai.
......
......@@ -50,6 +50,7 @@ class JSONLayoutResultsGenerator(json_results_generator.JSONResultsGeneratorBase
test_expectations.IMAGE: "I",
test_expectations.TEXT: "F",
test_expectations.MISSING: "O",
test_expectations.AUDIO: "A",
test_expectations.IMAGE_PLUS_TEXT: "Z"}
def __init__(self, port, builder_name, build_name, build_number,
......
......@@ -82,7 +82,7 @@ class SingleTestRunner:
# For example, if 'foo.html' has two expectation files, 'foo-expected.html' and
# 'foo-expected.txt', we should warn users. One test file must be used exclusively
# in either layout tests or reftests, but not in both.
for suffix in ['.txt', '.checksum', '.png']:
for suffix in ('.txt', '.checksum', '.png', '.wav'):
expected_filename = self._port.expected_filename(self._filename, suffix)
if fs.exists(expected_filename):
_log.error('The reftest (%s) can not have an expectation file (%s).'
......@@ -91,7 +91,8 @@ class SingleTestRunner:
def _expected_driver_output(self):
return base.DriverOutput(self._port.expected_text(self._filename),
self._port.expected_image(self._filename),
self._port.expected_checksum(self._filename))
self._port.expected_checksum(self._filename),
self._port.expected_audio(self._filename))
def _should_fetch_expected_checksum(self):
return (self._options.pixel_tests and
......@@ -142,6 +143,9 @@ class SingleTestRunner:
# DumpRenderTree may not output utf-8 text (e.g. webarchives).
self._save_baseline_data(driver_output.text, ".txt",
generate_new_baseline=self._options.new_baseline)
if driver_output.audio:
self._save_baseline_data(driver_output.audio, '.wav',
generate_new_baseline=self._options.new_baseline)
if self._options.pixel_tests and driver_output.image_hash:
self._save_baseline_data(driver_output.image, ".png",
generate_new_baseline=self._options.new_baseline)
......@@ -216,6 +220,7 @@ class SingleTestRunner:
return TestResult(self._filename, failures, driver_output.test_time)
failures.extend(self._compare_text(driver_output.text, expected_driver_output.text))
failures.extend(self._compare_audio(driver_output.audio, expected_driver_output.audio))
if self._options.pixel_tests:
failures.extend(self._compare_image(driver_output, expected_driver_output))
return TestResult(self._filename, failures, driver_output.test_time)
......@@ -230,6 +235,15 @@ class SingleTestRunner:
failures.append(test_failures.FailureMissingResult())
return failures
def _compare_audio(self, actual_audio, expected_audio):
failures = []
if (expected_audio and actual_audio and
self._port.compare_audio(actual_audio, expected_audio)):
failures.append(test_failures.FailureAudioMismatch())
elif actual_audio and not expected_audio:
failures.append(test_failures.FailureMissingAudio())
return failures
def _get_normalized_output_text(self, output):
"""Returns the normalized text output, i.e. the output in which
the end-of-line characters are normalized to "\n"."""
......
......@@ -41,8 +41,8 @@ _log = logging.getLogger("webkitpy.layout_tests.layout_package."
"test_expectations")
# Test expectation and modifier constants.
(PASS, FAIL, TEXT, IMAGE, IMAGE_PLUS_TEXT, TIMEOUT, CRASH, SKIP, WONTFIX,
SLOW, REBASELINE, MISSING, FLAKY, NOW, NONE) = range(15)
(PASS, FAIL, TEXT, IMAGE, IMAGE_PLUS_TEXT, AUDIO, TIMEOUT, CRASH, SKIP, WONTFIX,
SLOW, REBASELINE, MISSING, FLAKY, NOW, NONE) = range(16)
# Test expectation file update action constants
(NO_CHANGE, REMOVE_TEST, REMOVE_PLATFORM, ADD_PLATFORMS_EXCEPT_THIS) = range(4)
......@@ -120,7 +120,8 @@ class TestExpectations:
self._expected_failures.get_test_set(REBASELINE, IMAGE) |
self._expected_failures.get_test_set(REBASELINE, TEXT) |
self._expected_failures.get_test_set(REBASELINE,
IMAGE_PLUS_TEXT))
IMAGE_PLUS_TEXT) |
self._expected_failures.get_test_set(REBASELINE, AUDIO))
def get_options(self, test):
return self._expected_failures.get_options(test)
......@@ -244,11 +245,11 @@ class TestExpectationsFile:
Notes:
-A test cannot be both SLOW and TIMEOUT
-A test should only be one of IMAGE, TEXT, IMAGE+TEXT, or FAIL. FAIL is
a migratory state that currently means either IMAGE, TEXT, or
IMAGE+TEXT. Once we have finished migrating the expectations, we will
change FAIL to have the meaning of IMAGE+TEXT and remove the IMAGE+TEXT
identifier.
-A test should only be one of IMAGE, TEXT, IMAGE+TEXT, AUDIO, or FAIL.
FAIL is a legacy value that currently means either IMAGE,
TEXT, or IMAGE+TEXT. Once we have finished migrating the expectations,
we should change FAIL to have the meaning of IMAGE+TEXT and remove the
IMAGE+TEXT identifier.
-A test can be included twice, but not via the same path.
-If a test is included twice, then the more precise path wins.
-CRASH tests cannot be WONTFIX
......@@ -259,6 +260,7 @@ class TestExpectationsFile:
'text': TEXT,
'image': IMAGE,
'image+text': IMAGE_PLUS_TEXT,
'audio': AUDIO,
'timeout': TIMEOUT,
'crash': CRASH,
'missing': MISSING}
......@@ -271,6 +273,7 @@ class TestExpectationsFile:
IMAGE: ('image mismatch', 'image mismatch'),
IMAGE_PLUS_TEXT: ('image and text mismatch',
'image and text mismatch'),
AUDIO: ('audio mismatch', 'audio mismatch'),
CRASH: ('DumpRenderTree crash',
'DumpRenderTree crashes'),
TIMEOUT: ('test timed out', 'tests timed out'),
......@@ -278,7 +281,7 @@ class TestExpectationsFile:
'no expected results found')}
EXPECTATION_ORDER = (PASS, CRASH, TIMEOUT, MISSING, IMAGE_PLUS_TEXT,
TEXT, IMAGE, FAIL, SKIP)
TEXT, IMAGE, AUDIO, FAIL, SKIP)
BUILD_TYPES = ('debug', 'release')
......
......@@ -54,7 +54,8 @@ def determine_result_type(failure_list):
return test_expectations.TIMEOUT
elif (FailureMissingResult in failure_types or
FailureMissingImage in failure_types or
FailureMissingImageHash in failure_types):
FailureMissingImageHash in failure_types or
FailureMissingAudio in failure_types):
return test_expectations.MISSING
else:
is_text_failure = FailureTextMismatch in failure_types
......@@ -62,12 +63,15 @@ def determine_result_type(failure_list):
FailureImageHashMismatch in failure_types)
is_reftest_failure = (FailureReftestMismatch in failure_types or
FailureReftestMismatchDidNotOccur in failure_types)
is_audio_failure = (FailureAudioMismatch in failure_types)
if is_text_failure and is_image_failure:
return test_expectations.IMAGE_PLUS_TEXT
elif is_text_failure:
return test_expectations.TEXT
elif is_image_failure or is_reftest_failure:
return test_expectations.IMAGE
elif is_audio_failure:
return test_expectations.AUDIO
else:
raise ValueError("unclassifiable set of failures: "
+ str(failure_types))
......@@ -331,6 +335,28 @@ class FailureReftestMismatchDidNotOccur(ComparisonTestFailure):
return ' '.join(links)
class FailureMissingAudio(ComparisonTestFailure):
"""Actual result image was missing."""
OUT_FILENAMES = ("-actual.wav",)
@staticmethod
def message():
return "No expected audio found"
def result_html_output(self, filename):
return ("<strong>%s</strong>" % self.message() +
self.output_links(filename, self.OUT_FILENAMES))
class FailureAudioMismatch(ComparisonTestFailure):
"""Audio files didn't match."""
OUT_FILENAMES = ("-actual.wav", "-expected.wav")
@staticmethod
def message():
return "Audio mismatch"
# Convenient collection of all failure classes for anything that might
# need to enumerate over them all.
ALL_FAILURE_CLASSES = (FailureTimeout, FailureCrash, FailureMissingResult,
......
......@@ -63,6 +63,9 @@ def write_test_result(port, root_output_dir, filename, driver_output,
if not images_are_different:
checksums_mismatch_but_images_are_same = True
imagehash_mismatch_failure = failure
elif isinstance(failure, (test_failures.FailureAudioMismatch,
test_failures.FailureMissingAudio)):
writer.write_audio_files(driver_output.audio, expected_driver_output.audio)
elif isinstance(failure, test_failures.FailureCrash):
if failure.reference_filename:
writer.write_crash_report(expected_driver_output.error)
......@@ -187,6 +190,9 @@ class TestResultWriter(object):
pretty_patch_filename = self.output_filename(self.FILENAME_SUFFIX_PRETTY_PATCH)
fs.write_binary_file(pretty_patch_filename, pretty_patch)
def write_audio_files(self, actual_audio, expected_audio):
self.write_output_files('.wav', actual_audio, expected_audio)
def write_image_files(self, actual_image, expected_image):
self.write_output_files('.png', actual_image, expected_image)
......
......@@ -206,6 +206,10 @@ class Port(object):
interface so that it can be overriden for testing purposes."""
return expected_text != actual_text
def compare_audio(self, expected_audio, actual_audio):
"""Return whether the two audio files are *not* equal."""
return expected_audio != actual_audio
def diff_image(self, expected_contents, actual_contents,
diff_filename=None, tolerance=0):
"""Compare two images and produce a delta image file.
......@@ -351,6 +355,12 @@ class Port(object):
return None
return self._filesystem.read_binary_file(path)
def expected_audio(self, test):
path = self.expected_filename(test, '.wav')
if not self.path_exists(path):
return None
return self._filesystem.read_binary_file(path)
def expected_text(self, test):
"""Returns the text output we expect the test to produce, or None
if we don't expect there to be any text output.
......@@ -868,22 +878,24 @@ class DriverInput(object):
class DriverOutput(object):
"""Groups information about a output from driver for easy passing of data."""
def __init__(self, text, image, image_hash,
crash=False, test_time=None, timeout=False, error=''):
def __init__(self, text, image, image_hash, audio,
crash=False, test_time=0, timeout=False, error=''):
"""Initializes a TestOutput object.
Args:
text: a text output
image: an image output
image_hash: a string containing the checksum of the image
audio: contents of an audio stream, if any (in WAV format)
crash: a boolean indicating whether the driver crashed on the test
test_time: a time which the test has taken
test_time: the time the test took to execute
timeout: a boolean indicating whehter the test timed out
error: any unexpected or additional (or error) text output
"""
self.text = text
self.image = image
self.image_hash = image_hash
self.audio = audio
self.crash = crash
self.test_time = test_time
self.timeout = timeout
......
......@@ -500,14 +500,16 @@ class ChromiumDriver(base.Driver):
(line, crash) = self._write_command_and_read_line(input=None)
# FIXME: Add support for audio when we're ready.
run_time = time.time() - start_time
output_image = self._output_image_with_retry()
text = ''.join(output)
if not text:
text = None
return base.DriverOutput(text, output_image, actual_checksum,
crash, run_time, timeout, ''.join(error))
return base.DriverOutput(text, output_image, actual_checksum, audio=None,
crash=crash, test_time=run_time, timeout=timeout, error=''.join(error))
def stop(self):
if self._proc:
......
......@@ -119,29 +119,25 @@ class DryrunDriver(base.Driver):
def run_test(self, driver_input):
start_time = time.time()
fs = self._port._filesystem
if fs.exists(self._port.reftest_expected_filename(driver_input.filename)) or \
fs.exists(self._port.reftest_expected_mismatch_filename(driver_input.filename)):
text_output = 'test-text'
if (fs.exists(self._port.reftest_expected_filename(driver_input.filename)) or
fs.exists(self._port.reftest_expected_mismatch_filename(driver_input.filename)) or
driver_input.filename.endswith('-expected.html')):
text = 'test-text'
image = 'test-image'
hash = 'test-checksum'
elif driver_input.filename.endswith('-expected.html'):
text_output = 'test-text'
image = 'test-image'
hash = 'test-checksum'
checksum = 'test-checksum'
audio = None
elif driver_input.filename.endswith('-expected-mismatch.html'):
text_output = 'test-text-mismatch'
text = 'test-text-mismatch'
image = 'test-image-mismatch'
hash = 'test-checksum-mismatch'
elif driver_input.image_hash is not None:
text_output = self._port.expected_text(driver_input.filename)
image = self._port.expected_image(driver_input.filename)
hash = self._port.expected_checksum(driver_input.filename)
checksum = 'test-checksum-mismatch'
audio = None
else:
text_output = self._port.expected_text(driver_input.filename)
image = None
hash = None
return base.DriverOutput(text_output, image, hash, False,
time.time() - start_time, False, '')
text = self._port.expected_text(driver_input.filename)
image = self._port.expected_image(driver_input.filename)
checksum = self._port.expected_checksum(driver_input.filename)
audio = self._port.expected_audio(driver_input.filename)
return base.DriverOutput(text, image, checksum, audio, crash=False,
test_time=time.time() - start_time, timeout=False, error='')
def start(self):
pass
......
......@@ -32,6 +32,7 @@ This is an implementation of the Port interface that overrides other
ports and changes the Driver binary to "MockDRT".
"""
import base64
import logging
import optparse
import os
......@@ -206,15 +207,23 @@ class MockDRT(object):
test_path = test_input.uri
actual_text = port.expected_text(test_path)
actual_audio = port.expected_audio(test_path)
if self._options.pixel_tests and test_input.checksum:
actual_checksum = port.expected_checksum(test_path)
actual_image = port.expected_image(test_path)
self._stdout.write('Content-Type: text/plain\n')
if actual_audio:
self._stdout.write('Content-Type: audio/wav\n')
self._stdout.write('Content-Transfer-Encoding: base64\n')
output = base64.b64encode(actual_audio)
self._stdout.write('Content-Length: %s\n' % len(output))
self._stdout.write(output)
else:
self._stdout.write('Content-Type: text/plain\n')
# FIXME: Note that we don't ensure there is a trailing newline!
# This mirrors actual (Mac) DRT behavior but is a bug.
self._stdout.write(actual_text)
# FIXME: Note that we don't ensure there is a trailing newline!
# This mirrors actual (Mac) DRT behavior but is a bug.
self._stdout.write(actual_text)
self._stdout.write('#EOF\n')
if self._options.pixel_tests and test_input.checksum:
......@@ -223,7 +232,7 @@ class MockDRT(object):
self._stdout.write('ExpectedHash: %s\n' % test_input.checksum)
if actual_checksum != test_input.checksum:
self._stdout.write('Content-Type: image/png\n')
self._stdout.write('Content-Length: %s\n\n' % len(actual_image))
self._stdout.write('Content-Length: %s\n' % len(actual_image))
self._stdout.write(actual_image)
self._stdout.write('#EOF\n')
self._stdout.flush()
......
......@@ -200,7 +200,7 @@ class MockDRTTest(unittest.TestCase):
'ActualHash: checksum-checksum\n',
'ExpectedHash: wrong-checksum\n',
'Content-Type: image/png\n',
'Content-Length: 13\n\n',
'Content-Length: 13\n',
'checksum\x8a-png',
'#EOF\n'])
......
......@@ -30,6 +30,7 @@
"""Dummy Port implementation used for testing."""
from __future__ import with_statement
import base64
import time
from webkitpy.common.system import filesystem_mock
......@@ -66,6 +67,8 @@ class TestInstance:
self.expected_checksum = self.actual_checksum
self.expected_image = self.actual_image
self.actual_audio = None
self.expected_audio = None
# This is an in-memory list of tests, what we want them to produce, and
# what we want to claim are the expected results.
......@@ -111,11 +114,20 @@ def unit_test_list():
tests.add('failures/expected/image_checksum.html',
actual_checksum='image_checksum_fail-checksum',
actual_image='image_checksum_fail-png')
tests.add('failures/expected/audio.html',
actual_audio=base64.b64encode('audio_fail-wav'), expected_audio='audio-wav',
actual_text=None, expected_text=None,
actual_image=None, expected_image=None,
actual_checksum=None, expected_checksum=None)
tests.add('failures/expected/keyboard.html', keyboard=True)
tests.add('failures/expected/missing_check.html',
expected_checksum=None,
expected_image=None)
tests.add('failures/expected/missing_image.html', expected_image=None)
tests.add('failures/expected/missing_audio.html', expected_audio=None,
actual_text=None, expected_text=None,
actual_image=None, expected_image=None,
actual_checksum=None, expected_checksum=None)
tests.add('failures/expected/missing_text.html', expected_text=None)
tests.add('failures/expected/newlines_leading.html',
expected_text="\nfoo\n", actual_text="foo\n")
......@@ -134,6 +146,11 @@ def unit_test_list():
tests.add('http/tests/ssl/text.html')
tests.add('passes/error.html', error='stuff going to stderr')
tests.add('passes/image.html')
tests.add('passes/audio.html',
actual_audio=base64.b64encode('audio-wav'), expected_audio='audio-wav',
actual_text=None, expected_text=None,
actual_image=None, expected_image=None,
actual_checksum=None, expected_checksum=None)
tests.add('passes/platform_image.html')
tests.add('passes/checksum_in_image.html',
expected_checksum=None,
......@@ -184,20 +201,27 @@ def unit_test_filesystem(files=None):
add_file(files, test, '.html', '')
if test.is_reftest:
continue
if test.actual_audio:
add_file(files, test, '-expected.wav', test.expected_audio)
continue
add_file(files, test, '-expected.txt', test.expected_text)
add_file(files, test, '-expected.checksum', test.expected_checksum)
add_file(files, test, '-expected.png', test.expected_image)
# Add the test_expectations file.
files[LAYOUT_TEST_DIR + '/platform/test/test_expectations.txt'] = """
WONTFIX : failures/expected/checksum.html = IMAGE
WONTFIX : failures/expected/crash.html = CRASH
// This one actually passes because the checksums will match.
WONTFIX : failures/expected/image.html = PASS
WONTFIX : failures/expected/audio.html = AUDIO
WONTFIX : failures/expected/image_checksum.html = IMAGE
WONTFIX : failures/expected/mismatch.html = IMAGE
WONTFIX : failures/expected/missing_check.html = MISSING PASS
WONTFIX : failures/expected/missing_image.html = MISSING PASS
WONTFIX : failures/expected/missing_audio.html = MISSING PASS
WONTFIX : failures/expected/missing_text.html = MISSING PASS
WONTFIX : failures/expected/newlines_leading.html = TEXT
WONTFIX : failures/expected/newlines_trailing.html = TEXT
......@@ -412,10 +436,13 @@ class TestDriver(base.Driver):
raise ValueError('exception from ' + test_name)
if test.hang:
time.sleep((float(test_input.timeout) * 4) / 1000.0)
audio = None
if test.actual_audio:
audio = base64.b64decode(test.actual_audio)
return base.DriverOutput(test.actual_text, test.actual_image,
test.actual_checksum, test.crash,
time.time() - start_time, test.timeout,
test.error)
test.actual_checksum, audio, crash=test.crash,
test_time=time.time() - start_time, timeout=test.timeout, error=test.error)
def start(self):
pass
......
......@@ -30,7 +30,7 @@
"""WebKit implementations of the Port interface."""
import base64
import logging
import operator
import os
......@@ -406,51 +406,24 @@ class WebKitDriver(base.Driver):
start_time = time.time()
self._server_process.write(command)
have_seen_content_type = False
actual_image_hash = None
output = str() # Use a byte array for output, even though it should be UTF-8.
text = None
image = None
actual_image_hash = None
audio = None
deadline = time.time() + int(driver_input.timeout) / 1000.0
timeout = int(driver_input.timeout) / 1000.0
deadline = time.time() + timeout
line = self._server_process.read_line(timeout)
while (not self._server_process.timed_out
and not self._server_process.crashed
and line.rstrip() != "#EOF"):
if (line.startswith('Content-Type:') and not
have_seen_content_type):
have_seen_content_type = True
else:
# Note: Text output from DumpRenderTree is always UTF-8.
# However, some tests (e.g. webarchives) spit out binary
# data instead of text. So to make things simple, we
# always treat the output as binary.
output += line
line = self._server_process.read_line(timeout)
timeout = deadline - time.time()
if output:
text = output
# First block is either text or audio
block = self._read_block(deadline)
if block.content_type == 'audio/wav':
audio = block.decoded_content
else:
text = block.decoded_content
# Now read a second block of text for the optional image data
remaining_length = -1
HASH_HEADER = 'ActualHash: '
LENGTH_HEADER = 'Content-Length: '
line = self._server_process.read_line(timeout)
while (not self._server_process.timed_out
and not self._server_process.crashed
and line.rstrip() != "#EOF"):
if line.startswith(HASH_HEADER):
actual_image_hash = line[len(HASH_HEADER):].strip()
elif line.startswith('Content-Type:'):
pass
elif line.startswith(LENGTH_HEADER):
timeout = deadline - time.time()
content_length = int(line[len(LENGTH_HEADER):])
image = self._server_process.read(timeout, content_length)
timeout = deadline - time.time()
line = self._server_process.read_line(timeout)
# Now read an optional second block of image data
block = self._read_block(deadline)
if block.content and block.content_type == 'image/png':
image = block.decoded_content
actual_image_hash = block.content_hash
error_lines = self._server_process.error.splitlines()
# FIXME: This is a hack. It is unclear why sometimes
......@@ -462,13 +435,59 @@ class WebKitDriver(base.Driver):
# FIXME: This seems like the wrong section of code to be doing
# this reset in.
self._server_process.error = ""
return base.DriverOutput(text, image, actual_image_hash,
self._server_process.crashed,
time.time() - start_time,
self._server_process.timed_out,
error)
return base.DriverOutput(text, image, actual_image_hash, audio,
crash=self._server_process.crashed, test_time=time.time() - start_time,
timeout=self._server_process.timed_out, error=error)
def _read_block(self, deadline):
LENGTH_HEADER = 'Content-Length: '
HASH_HEADER = 'ActualHash: '
TYPE_HEADER = 'Content-Type: '
ENCODING_HEADER = 'Content-Transfer-Encoding: '
content_type = None
encoding = None
content_hash = None
content_length = None
# Content is treated as binary data even though the text output
# is usually UTF-8.
content = ''
timeout = deadline - time.time()
line = self._server_process.read_line(timeout)
while (not self._server_process.timed_out
and not self._server_process.crashed
and line.rstrip() != "#EOF"):
if line.startswith(TYPE_HEADER) and content_type is None:
content_type = line.split()[1]
elif line.startswith(ENCODING_HEADER) and encoding is None:
encoding = line.split()[1]
elif line.startswith(LENGTH_HEADER) and content_length is None:
timeout = deadline - time.time()
content_length = int(line[len(LENGTH_HEADER):])
# FIXME: Technically there should probably be another blank
# line here, but DRT doesn't write one.
content = self._server_process.read(timeout, content_length)
elif line.startswith(HASH_HEADER):
content_hash = line.split()[1]
else:
content += line
line = self._server_process.read_line(timeout)
timeout = deadline -