Merge lp:~stewart/percona-xtrabackup/1.6-testsuite-fixes into lp:percona-xtrabackup/1.6
- 1.6-testsuite-fixes
- Merge into release-1.6
Proposed by
Stewart Smith
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Stewart Smith | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 255 | ||||
Proposed branch: | lp:~stewart/percona-xtrabackup/1.6-testsuite-fixes | ||||
Merge into: | lp:percona-xtrabackup/1.6 | ||||
Diff against target: |
18017 lines (+16942/-219) 86 files modified
test/bootstrap.sh (+88/-0) test/experimental/xb_race_drop.sh (+0/-2) test/inc/common.sh (+63/-114) test/python/BytesIO.py (+136/-0) test/python/iso8601/LICENSE (+20/-0) test/python/iso8601/README (+26/-0) test/python/iso8601/README.subunit (+5/-0) test/python/iso8601/setup.py (+58/-0) test/python/iso8601/test_iso8601.py (+111/-0) test/python/junitxml/__init__.py (+221/-0) test/python/junitxml/tests/__init__.py (+16/-0) test/python/junitxml/tests/test_junitxml.py (+327/-0) test/python/subunit/__init__.py (+1250/-0) test/python/subunit/chunked.py (+185/-0) test/python/subunit/details.py (+119/-0) test/python/subunit/iso8601.py (+133/-0) test/python/subunit/progress_model.py (+106/-0) test/python/subunit/run.py (+73/-0) test/python/subunit/test_results.py (+492/-0) test/python/subunit/tests/TestUtil.py (+80/-0) test/python/subunit/tests/__init__.py (+41/-0) test/python/subunit/tests/sample-script.py (+21/-0) test/python/subunit/tests/sample-two-script.py (+7/-0) test/python/subunit/tests/test_chunked.py (+152/-0) test/python/subunit/tests/test_details.py (+112/-0) test/python/subunit/tests/test_progress_model.py (+118/-0) test/python/subunit/tests/test_subunit_filter.py (+208/-0) test/python/subunit/tests/test_subunit_stats.py (+84/-0) test/python/subunit/tests/test_subunit_tags.py (+69/-0) test/python/subunit/tests/test_tap2subunit.py (+445/-0) test/python/subunit/tests/test_test_protocol.py (+1299/-0) test/python/subunit/tests/test_test_results.py (+300/-0) test/python/testtools/__init__.py (+80/-0) test/python/testtools/_spinner.py (+316/-0) test/python/testtools/compat.py (+286/-0) test/python/testtools/content.py (+238/-0) test/python/testtools/content_type.py (+33/-0) test/python/testtools/deferredruntest.py (+335/-0) test/python/testtools/distutilscmd.py (+62/-0) test/python/testtools/helpers.py (+64/-0) test/python/testtools/matchers.py (+785/-0) test/python/testtools/monkey.py (+97/-0) test/python/testtools/run.py (+332/-0) test/python/testtools/runtest.py (+200/-0) test/python/testtools/testcase.py (+724/-0) test/python/testtools/testresult/__init__.py (+19/-0) test/python/testtools/testresult/doubles.py (+111/-0) test/python/testtools/testresult/real.py (+621/-0) test/python/testtools/tests/__init__.py (+44/-0) test/python/testtools/tests/helpers.py (+72/-0) test/python/testtools/tests/test_compat.py (+257/-0) test/python/testtools/tests/test_content.py (+223/-0) test/python/testtools/tests/test_content_type.py (+46/-0) test/python/testtools/tests/test_deferredruntest.py (+738/-0) test/python/testtools/tests/test_distutilscmd.py (+90/-0) test/python/testtools/tests/test_fixturesupport.py (+79/-0) test/python/testtools/tests/test_helpers.py (+106/-0) test/python/testtools/tests/test_matchers.py (+695/-0) test/python/testtools/tests/test_monkey.py (+167/-0) test/python/testtools/tests/test_run.py (+77/-0) test/python/testtools/tests/test_runtest.py (+300/-0) test/python/testtools/tests/test_spinner.py (+332/-0) test/python/testtools/tests/test_testresult.py (+1372/-0) test/python/testtools/tests/test_testsuite.py (+53/-0) test/python/testtools/tests/test_testtools.py (+1143/-0) test/python/testtools/tests/test_with_with.py (+42/-0) test/python/testtools/testsuite.py (+87/-0) test/python/testtools/utils.py (+13/-0) test/run.sh (+140/-14) test/subunit.sh (+56/-0) test/subunit2junitxml (+65/-0) test/t/bug606981.sh (+8/-6) test/t/ib_csm_csv.sh (+3/-5) test/t/ib_incremental.sh (+8/-9) test/t/ib_partition.sh (+0/-4) test/t/ib_specialchar.sh (+3/-6) test/t/xb_basic.sh (+3/-5) test/t/xb_export.sh (+12/-8) test/t/xb_incremental.sh (+7/-7) test/t/xb_incremental_compressed.sh (+15/-13) test/t/xb_parallel.sh (+3/-5) test/t/xb_part_range.sh (+0/-3) test/t/xb_partial.sh (+3/-4) test/t/xb_stats.sh (+6/-8) test/t/xb_stream.sh (+3/-5) utils/build.sh (+3/-1) |
||||
To merge this branch: | bzr merge lp:~stewart/percona-xtrabackup/1.6-testsuite-fixes | ||||
Related bugs: |
|
||||
Related blueprints: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Percona developers | Pending | ||
Review via email: mp+64140@code.launchpad.net |
Commit message
Description of the change
Includes Alexey's fixes, plus some more of mine.
big change is to make run.sh also output subunit, which subunit2junitxml then converts to the JUnit that Jenkins understands. (see http://
I've included needed libraries to run subunit2junitxml and updated the Jenkins percona-
http://
(also see branch that is merged with trunk)
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file 'test/bootstrap.sh' |
2 | --- test/bootstrap.sh 1970-01-01 00:00:00 +0000 |
3 | +++ test/bootstrap.sh 2011-06-10 09:54:56 +0000 |
4 | @@ -0,0 +1,88 @@ |
5 | +#!/bin/bash |
6 | + |
7 | +set -e |
8 | + |
9 | +function usage() |
10 | +{ |
11 | + cat <<EOF |
12 | +Usage: |
13 | +$0 xtrabackup_target [installation_directory] |
14 | +$0 path_to_server_tarball [installation_directory] |
15 | + |
16 | +Prepares a server binary directory to be used by run.sh when running XtraBackup |
17 | +tests. |
18 | + |
19 | +If the argument is one of the build targets passed to build.sh |
20 | +(i.e. innodb51_builtin innodb51 innodb55 xtradb51 xtradb55) then the |
21 | +appropriate Linux tarball is downloaded from a pre-defined location and |
22 | +unpacked into the specified installation directory ('./server' by default). |
23 | + |
24 | +Otherwise the argument is assumed to be a path to a server binary tarball. |
25 | +EOF |
26 | +} |
27 | + |
28 | +if [ -z "$1" ] |
29 | +then |
30 | + usage |
31 | +fi |
32 | + |
33 | +arch="`uname -m`" |
34 | +if [ "$arch" = "i386" ] |
35 | +then |
36 | + arch="i686" |
37 | +fi |
38 | + |
39 | +case "$1" in |
40 | + innodb51_builtin | innodb51) |
41 | + url="http://www.percona.com/downloads/community/" |
42 | + tarball="mysql-5.1.49-linux-$arch-glibc23.tar.gz" |
43 | + ;; |
44 | + innodb55) |
45 | + url="http://www.percona.com/downloads/community/" |
46 | + tarball="mysql-5.5.6-rc-linux2.6-$arch.tar.gz" |
47 | + ;; |
48 | + xtradb51) |
49 | + url="http://www.percona.com/downloads/community/" |
50 | + tarball="Percona-Server-5.1.51-rel11.5-132-Linux-$arch.tar.gz" |
51 | + ;; |
52 | + xtradb55) |
53 | + url="http://www.percona.com/downloads/Percona-Server-5.5/Percona-Server-5.5.11-20.2/Linux/binary/" |
54 | + tarball="Percona-Server-5.5.11-rel20.2-116.Linux.$arch.tar.gz" |
55 | + ;; |
56 | + *) |
57 | + if ! test -r "$1" |
58 | + then |
59 | + echo "$1 does not exist" |
60 | + exit 1 |
61 | + fi |
62 | + tarball="$1" |
63 | + ;; |
64 | +esac |
65 | + |
66 | +if test -n "$2" |
67 | +then |
68 | + destdir="$2" |
69 | +else |
70 | + destdir="./server" |
71 | +fi |
72 | + |
73 | +if test -n "$url" |
74 | +then |
75 | + echo "Downloading $tarball" |
76 | + wget -c "$url/$tarball" |
77 | +fi |
78 | + |
79 | +if test -d "$destdir" |
80 | +then |
81 | + rm -rf "$destdir" |
82 | +fi |
83 | +mkdir "$destdir" |
84 | + |
85 | +echo "Unpacking $tarball into $destdir" |
86 | +tar zxf $tarball -C $destdir |
87 | +sourcedir="$destdir/`ls $destdir`" |
88 | +if test -n "$sourcedir" |
89 | +then |
90 | + mv $sourcedir/* $destdir |
91 | + rm -rf $sourcedir |
92 | +fi |
93 | |
94 | === modified file 'test/experimental/xb_race_drop.sh' |
95 | --- test/experimental/xb_race_drop.sh 2011-03-01 16:59:27 +0000 |
96 | +++ test/experimental/xb_race_drop.sh 2011-06-10 09:54:56 +0000 |
97 | @@ -11,5 +11,3 @@ |
98 | vlog "Starting backup" |
99 | xtrabackup --datadir=$mysql_datadir --backup --target-dir=$topdir/data/full |
100 | vlog "Full backup done" |
101 | -stop_mysqld |
102 | -clean |
103 | |
104 | === modified file 'test/inc/common.sh' |
105 | --- test/inc/common.sh 2011-03-14 18:46:12 +0000 |
106 | +++ test/inc/common.sh 2011-06-10 09:54:56 +0000 |
107 | @@ -1,16 +1,20 @@ |
108 | #!/bin/env bash |
109 | |
110 | set -eu |
111 | -topdir="`pwd`/var" |
112 | -mysql_datadir="$topdir/mysql" |
113 | -mysql_port="3306" |
114 | -mysql_socket=${mysql_socket:-"/tmp/xtrabackup.mysql.sock"} |
115 | - |
116 | -OUTFILE=results/`basename $0`_innobackupex.out |
117 | + |
118 | +function innobackupex() |
119 | +{ |
120 | + run_cmd `which innobackupex` $IB_ARGS $* |
121 | +} |
122 | + |
123 | +function xtrabackup() |
124 | +{ |
125 | + run_cmd `which $XB_BIN` $XB_ARGS $* |
126 | +} |
127 | |
128 | function vlog |
129 | { |
130 | - echo "`date +"%F %T"`: `basename "$0"`: $@" |
131 | + echo "`date +"%F %T"`: `basename "$0"`: $@" >> $OUTFILE |
132 | } |
133 | |
134 | function clean_datadir() |
135 | @@ -53,105 +57,6 @@ |
136 | mkdir -p "$mysql_datadir" |
137 | } |
138 | |
139 | -function init_mysql() |
140 | -{ |
141 | - url="http://www.percona.com/downloads/community/" |
142 | - case "$MYSQL_VERSION" in |
143 | - system) |
144 | - echo "Using MySQL installed in the system" |
145 | - MYSQL=mysql |
146 | - MYSQLADMIN=mysqladmin |
147 | - MYSQL_INSTALL_DB=mysql_install_db |
148 | - MYSQL_ARGS="--no-defaults --socket=${mysql_socket} --user=root" |
149 | - MYSQLD=mysqld |
150 | - MYSQL_BASEDIR="/usr" |
151 | - MYSQLD_ARGS="--no-defaults --basedir=${MYSQL_BASEDIR} --socket=${mysql_socket} --datadir=$mysql_datadir" |
152 | - IB_BIN="innobackupex --defaults-file=$topdir/my.cnf" |
153 | - XB_BIN=xtrabackup |
154 | - ;; |
155 | - 5.0) |
156 | - echo "Using MySQL 5.0" |
157 | - version="5.0.91-linux-`uname -m`-glibc23" |
158 | - cd $topdir |
159 | - wget "$url/mysql-$version.tar.gz" >/dev/null 2>&1 |
160 | - tar zxf mysql-$version.tar.gz |
161 | - MYSQL=$topdir/mysql-$version/bin/mysql |
162 | - MYSQLADMIN=$topdir/mysql-$version/bin/mysqladmin |
163 | - MYSQL_INSTALL_DB=$topdir/mysql-$version/scripts/mysql_install_db |
164 | - MYSQLD=$topdir/mysql-$version/bin/mysqld |
165 | - MYSQL_BASEDIR=$topdir/mysql-$version |
166 | - MYSQLD_ARGS="--no-defaults --basedir=${MYSQL_BASEDIR} --socket=${mysql_socket} --datadir=$mysql_datadir" |
167 | - MYSQL_ARGS="--no-defaults --socket=${mysql_socket} --user=root" |
168 | - IB_BIN="innobackupex --defaults-file=$topdir/my.cnf" |
169 | - XB_BIN=xtrabackup_51 |
170 | - cd - |
171 | - ;; |
172 | - 5.1) |
173 | - echo "Using MySQL 5.1" |
174 | - version="5.1.49-linux-`uname -m`-glibc23" |
175 | - cd $topdir |
176 | - wget "$url/mysql-$version.tar.gz" >/dev/null 2>&1 |
177 | - tar zxf mysql-$version.tar.gz |
178 | - MYSQL=$topdir/mysql-$version/bin/mysql |
179 | - MYSQLADMIN=$topdir/mysql-$version/bin/mysqladmin |
180 | - MYSQL_INSTALL_DB=$topdir/mysql-$version/scripts/mysql_install_db |
181 | - MYSQLD=$topdir/mysql-$version/bin/mysqld |
182 | - MYSQL_BASEDIR=$topdir/mysql-$version |
183 | - MYSQLD_ARGS="--no-defaults --basedir=${MYSQL_BASEDIR} --socket=${mysql_socket} --datadir=$mysql_datadir" |
184 | - MYSQL_ARGS="--no-defaults --socket=${mysql_socket} --user=root" |
185 | - IB_BIN="innobackupex --defaults-file=$topdir/my.cnf" |
186 | - XB_BIN=xtrabackup |
187 | - cd - |
188 | - ;; |
189 | - 5.5) |
190 | - echo "Using MySQL 5.5" |
191 | - version="5.5.6-rc-linux2.6-`uname -m`" |
192 | - cd $topdir |
193 | - wget "$url/mysql-$version.tar.gz" >/dev/null 2>&1 |
194 | - tar zxf mysql-$version.tar.gz |
195 | - MYSQL=$topdir/mysql-$version/bin/mysql |
196 | - MYSQLADMIN=$topdir/mysql-$version/bin/mysqladmin |
197 | - MYSQL_INSTALL_DB=$topdir/mysql-$version/scripts/mysql_install_db |
198 | - MYSQLD=$topdir/mysql-$version/bin/mysqld |
199 | - MYSQL_BASEDIR=$topdir/mysql-$version |
200 | - MYSQLD_ARGS="--no-defaults --basedir=${MYSQL_BASEDIR} --socket=${mysql_socket} --datadir=$mysql_datadir" |
201 | - MYSQL_ARGS="--no-defaults --socket=${mysql_socket} --user=root" |
202 | - IB_BIN="innobackupex --defaults-file=$topdir/my.cnf" |
203 | - XB_BIN=xtrabackup_55 |
204 | - cd - |
205 | - ;; |
206 | - percona) |
207 | - echo "Using Percona Server" |
208 | - version="5.1.51-rel11.5-132-Linux-`uname -m`" |
209 | - cd $topdir |
210 | - wget "$url/Percona-Server-$version.tar.gz" >/dev/null 2>&1 |
211 | - tar zxf Percona-Server-$version.tar.gz |
212 | - MYSQL=$topdir/Percona-Server-$version/bin/mysql |
213 | - MYSQLADMIN=$topdir/Percona-Server-$version/bin/mysqladmin |
214 | - MYSQL_INSTALL_DB=$topdir/Percona-Server-$version/bin/mysql_install_db |
215 | - MYSQLD=$topdir/Percona-Server-$version/libexec/mysqld |
216 | - MYSQL_BASEDIR=$topdir/Percona-Server-$version |
217 | - MYSQLD_ARGS="--no-defaults --basedir=${MYSQL_BASEDIR} --socket=${mysql_socket} --datadir=$mysql_datadir" |
218 | - MYSQL_ARGS="--no-defaults --socket=${mysql_socket} --user=root" |
219 | - set +u |
220 | - export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$topdir/Percona-Server-$version/lib/mysql |
221 | - set -u |
222 | - IB_BIN="innobackupex --defaults-file=$topdir/my.cnf" |
223 | - XB_BIN=xtrabackup |
224 | - |
225 | - cd - |
226 | - ;; |
227 | - *) |
228 | - usage |
229 | - exit -1 |
230 | - ;; |
231 | - esac |
232 | - if [ "`whoami`" = "root" ] |
233 | - then |
234 | - MYSQLD_ARGS="$MYSQLD_ARGS --user=root" |
235 | - fi |
236 | -} |
237 | - |
238 | function init_mysql_dir() |
239 | { |
240 | vlog "Creating MySQL database" |
241 | @@ -189,13 +94,45 @@ |
242 | result="0" |
243 | fi |
244 | echo $result |
245 | - } |
246 | - |
247 | +} |
248 | + |
249 | +function get_xtrabackup_version() |
250 | +{ |
251 | + if [ "${MYSQL_VERSION:0:3}" = "5.1" ] |
252 | + then |
253 | + if [ -z "$INNODB_VERSION" ] |
254 | + then |
255 | + XB_BIN="xtrabackup_51" # InnoDB 5.1 builtin |
256 | + else |
257 | + XB_BIN="xtrabackup" # InnoDB 5.1 plugin or Percona Server 5.1 |
258 | + fi |
259 | + elif [ "${MYSQL_VERSION:0:3}" = "5.5" ] |
260 | + then |
261 | + XB_BIN="xtrabackup_55" |
262 | + else |
263 | + vlog "Uknown MySQL/InnoDB version: $MYSQL_VERSION/$INNODB_VERSION" |
264 | + exit -1 |
265 | + fi |
266 | + XTRADB_VERSION=`echo $INNODB_VERSION | sed 's/[0-9]\.[0-9]\.[0-9][0-9]*\(-[0-9][0-9]*\.[0-9][0-9]*\)*$/\1/'` |
267 | + vlog "Using '$XB_BIN' as xtrabackup binary" |
268 | + # Set the correct binary for innobackupex |
269 | + IB_ARGS="$IB_ARGS --ibbackup=$XB_BIN" |
270 | +} |
271 | + |
272 | +function kill_leftovers() |
273 | +{ |
274 | + while test -f "$PWD/mysqld.pid" |
275 | + do |
276 | + vlog "Found a leftover mysqld processes with PID `cat $PWD/mysqld.pid`, stopping it" |
277 | + stop_mysqld 2> /dev/null |
278 | + done |
279 | +} |
280 | |
281 | function run_mysqld() |
282 | { |
283 | local c=0 |
284 | - ${MYSQLD} ${MYSQLD_ARGS} $* --port=$mysql_port & |
285 | + kill_leftovers |
286 | + ${MYSQLD} ${MYSQLD_ARGS} $* --pid-file="$PWD/mysqld.pid" & |
287 | while [ "`mysql_ping`" != "1" ] |
288 | do |
289 | if [ ${c} -eq 100 ] |
290 | @@ -206,8 +143,13 @@ |
291 | let c=${c}+1 |
292 | sleep 1 |
293 | done |
294 | + # Get MySQL and InnoDB versions |
295 | + MYSQL_VERSION=`$MYSQL ${MYSQL_ARGS} -Nsf -e "SHOW VARIABLES LIKE 'version'"` |
296 | + MYSQL_VERSION=${MYSQL_VERSION#"version "} |
297 | + INNODB_VERSION=`$MYSQL ${MYSQL_ARGS} -Nsf -e "SHOW VARIABLES LIKE 'innodb_version'"` |
298 | + INNODB_VERSION=${INNODB_VERSION#"innodb_version "} |
299 | + get_xtrabackup_version |
300 | vlog "MySQL started successfully" |
301 | - |
302 | } |
303 | |
304 | function run_cmd() |
305 | @@ -218,8 +160,17 @@ |
306 | |
307 | function stop_mysqld() |
308 | { |
309 | - ${MYSQLADMIN} ${MYSQL_ARGS} shutdown |
310 | - vlog "Database server has been stopped" |
311 | + if [ -f "$PWD/mysqld.pid" ] |
312 | + then |
313 | + ${MYSQLADMIN} ${MYSQL_ARGS} shutdown |
314 | + vlog "Database server has been stopped" |
315 | + if [ -f "$PWD/mysqld.pid" ] |
316 | + then |
317 | + sleep 1; |
318 | + kill -9 `cat $PWD/mysqld.pid` |
319 | + rm -f $PWD/mysqld.pid |
320 | + fi |
321 | + fi |
322 | } |
323 | |
324 | function load_sakila() |
325 | @@ -254,9 +205,7 @@ |
326 | function init() |
327 | { |
328 | initdir |
329 | -init_mysql |
330 | init_mysql_dir |
331 | -set_mysl_port |
332 | echo " |
333 | [mysqld] |
334 | datadir=$mysql_datadir" > $topdir/my.cnf |
335 | |
336 | === added directory 'test/python' |
337 | === added file 'test/python/BytesIO.py' |
338 | --- test/python/BytesIO.py 1970-01-01 00:00:00 +0000 |
339 | +++ test/python/BytesIO.py 2011-06-10 09:54:56 +0000 |
340 | @@ -0,0 +1,136 @@ |
341 | + |
342 | +# http://wiki.python.org/moin/BytesIO |
343 | +# |
344 | +# A skeleton one used for systems that don't have BytesIO. |
345 | +# |
346 | +# It's enough for subunit at least.... |
347 | + |
348 | +class BytesIO(object): |
349 | + """ A file-like API for reading and writing bytes objects. |
350 | + |
351 | + Mostly like StringIO, but write() calls modify the underlying |
352 | + bytes object. |
353 | + |
354 | + >>> b = bytes() |
355 | + >>> f = BytesIO(b, 'w') |
356 | + >>> f.write(bytes.fromhex('ca fe ba be')) |
357 | + >>> f.write(bytes.fromhex('57 41 56 45')) |
358 | + >>> b |
359 | + bytes([202, 254, 186, 190, 87, 65, 86, 69]) |
360 | + """ |
361 | + |
362 | + def __init__(self, buf, mode='r'): |
363 | + """ Create a new BytesIO for reading or writing the given buffer. |
364 | + |
365 | + buf - Back-end buffer for this BytesIO. A bytes object. |
366 | + Actually, anything that supports len(), slice-assignment, |
367 | + and += will work. |
368 | + mode - One of 'r', 'w', 'a'. |
369 | + An optional 'b' is also allowed, but it doesn't do anything. |
370 | + """ |
371 | + # XXX many 'mode' possibilities aren't allowed yet: 'rw+Ut' |
372 | + if len(mode) == 2 and mode[-1] == 'b': |
373 | + mode = mode[:-1] # binary mode goes without saying |
374 | + if mode not in ('r', 'w', 'a'): |
375 | + raise ValueError("mode must be 'r', 'w', or 'a'") |
376 | + |
377 | + self._buf = buf |
378 | + self.mode = mode |
379 | + self.closed = False |
380 | + if self.mode == 'w': |
381 | + del buf[:] |
382 | + self._point = 0 |
383 | + elif self.mode == 'r': |
384 | + self._point = 0 |
385 | + else: # 'a' |
386 | + self._point = len(buf) |
387 | + |
388 | + def close(self): |
389 | + self.closed = True |
390 | + |
391 | + def _check_closed(self): |
392 | + if self.closed: |
393 | + raise ValueError("file is closed") |
394 | + |
395 | + def flush(self): |
396 | + self._check_closed() |
397 | + |
398 | + def next(self): |
399 | + line = self.readline() |
400 | + if len(line) == 0: |
401 | + raise StopIteration |
402 | + return line |
403 | + |
404 | + def read(self, size=None): |
405 | + self._check_closed() |
406 | + if size is None: |
407 | + e = len(self._buf) |
408 | + else: |
409 | + e = min(self._point + size, len(self._buf)) |
410 | + r = self._buf[self._point:e] |
411 | + self._point = e |
412 | + return r |
413 | + |
414 | + def readline(self, size=None): |
415 | + self._check_closed() |
416 | + die # XXX TODO - assume ascii and read a line |
417 | + |
418 | + def readlines(self, sizehint=None): |
419 | + # XXX TODO handle sizehint |
420 | + return list(self) |
421 | + |
422 | + def seek(self, offset, whence=0): |
423 | + self._check_closed() |
424 | + |
425 | + if whence == 0: |
426 | + self._point = offset |
427 | + elif whence == 1: |
428 | + self._point += offset |
429 | + elif whence == 2: |
430 | + self._point = len(self._buf) + offset |
431 | + else: |
432 | + raise ValueError("whence must be 0, 1, or 2") |
433 | + |
434 | + if self._point < 0: |
435 | + self._point = 0 # XXX is this right? |
436 | + |
437 | + def tell(self): |
438 | + self._check_closed() |
439 | + return self._point |
440 | + |
441 | + def truncate(self, size=None): |
442 | + self._check_closed() |
443 | + if size is None: |
444 | + size = self.tell() |
445 | + del self._buf[size:] |
446 | + |
447 | + def write(self, data): |
448 | + self._check_closed() |
449 | + amt = len(data) |
450 | + size = len(self._buf) |
451 | + if self.mode == 'a': |
452 | + self._point = size |
453 | + |
454 | + if self._point > size: |
455 | + if isinstance(b, bytes): |
456 | + blank = bytes([0]) |
457 | + else: |
458 | + # Don't know what default value to insert, unfortunately |
459 | + raise ValueError("can't write past the end of this object") |
460 | + self._buf += blank * (self._point - size) + data |
461 | + self._point = len(self._buf) |
462 | + else: |
463 | + p = self._point |
464 | + self._buf[p:p + amt] = data |
465 | + self._point = min(p + amt, len(self._buf)) |
466 | + |
467 | + def writelines(self, seq): |
468 | + for line in seq: |
469 | + self.write(line) |
470 | + |
471 | + def __iter__(self): |
472 | + return self |
473 | + |
474 | + @property |
475 | + def name(self): |
476 | + return repr(self) |
477 | |
478 | === added directory 'test/python/iso8601' |
479 | === added file 'test/python/iso8601/LICENSE' |
480 | --- test/python/iso8601/LICENSE 1970-01-01 00:00:00 +0000 |
481 | +++ test/python/iso8601/LICENSE 2011-06-10 09:54:56 +0000 |
482 | @@ -0,0 +1,20 @@ |
483 | +Copyright (c) 2007 Michael Twomey |
484 | + |
485 | +Permission is hereby granted, free of charge, to any person obtaining a |
486 | +copy of this software and associated documentation files (the |
487 | +"Software"), to deal in the Software without restriction, including |
488 | +without limitation the rights to use, copy, modify, merge, publish, |
489 | +distribute, sublicense, and/or sell copies of the Software, and to |
490 | +permit persons to whom the Software is furnished to do so, subject to |
491 | +the following conditions: |
492 | + |
493 | +The above copyright notice and this permission notice shall be included |
494 | +in all copies or substantial portions of the Software. |
495 | + |
496 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
497 | +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
498 | +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. |
499 | +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY |
500 | +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, |
501 | +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE |
502 | +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
503 | |
504 | === added file 'test/python/iso8601/README' |
505 | --- test/python/iso8601/README 1970-01-01 00:00:00 +0000 |
506 | +++ test/python/iso8601/README 2011-06-10 09:54:56 +0000 |
507 | @@ -0,0 +1,26 @@ |
508 | +A simple package to deal with ISO 8601 date time formats. |
509 | + |
510 | +ISO 8601 defines a neutral, unambiguous date string format, which also |
511 | +has the property of sorting naturally. |
512 | + |
513 | +e.g. YYYY-MM-DDTHH:MM:SSZ or 2007-01-25T12:00:00Z |
514 | + |
515 | +Currently this covers only the most common date formats encountered, not |
516 | +all of ISO 8601 is handled. |
517 | + |
518 | +Currently the following formats are handled: |
519 | + |
520 | +* 2006-01-01T00:00:00Z |
521 | +* 2006-01-01T00:00:00[+-]00:00 |
522 | + |
523 | +I'll add more as I encounter them in my day to day life. Patches with |
524 | +new formats and tests will be gratefully accepted of course :) |
525 | + |
526 | +References: |
527 | + |
528 | +* http://www.cl.cam.ac.uk/~mgk25/iso-time.html - simple overview |
529 | + |
530 | +* http://hydracen.com/dx/iso8601.htm - more detailed enumeration of |
531 | + valid formats. |
532 | + |
533 | +See the LICENSE file for the license this package is released under. |
534 | |
535 | === added file 'test/python/iso8601/README.subunit' |
536 | --- test/python/iso8601/README.subunit 1970-01-01 00:00:00 +0000 |
537 | +++ test/python/iso8601/README.subunit 2011-06-10 09:54:56 +0000 |
538 | @@ -0,0 +1,5 @@ |
539 | +This is a [slightly rearranged] import of http://pypi.python.org/pypi/iso8601/ |
540 | +version 0.1.4. The OS X hidden files have been stripped, and the package |
541 | +turned into a single module, to simplify installation. The remainder of the |
542 | +source distribution is included in the subunit source tree at python/iso8601 |
543 | +for reference. |
544 | |
545 | === added file 'test/python/iso8601/setup.py' |
546 | --- test/python/iso8601/setup.py 1970-01-01 00:00:00 +0000 |
547 | +++ test/python/iso8601/setup.py 2011-06-10 09:54:56 +0000 |
548 | @@ -0,0 +1,58 @@ |
549 | +try: |
550 | + from setuptools import setup |
551 | +except ImportError: |
552 | + from distutils import setup |
553 | + |
554 | +long_description="""Simple module to parse ISO 8601 dates |
555 | + |
556 | +This module parses the most common forms of ISO 8601 date strings (e.g. |
557 | +2007-01-14T20:34:22+00:00) into datetime objects. |
558 | + |
559 | +>>> import iso8601 |
560 | +>>> iso8601.parse_date("2007-01-25T12:00:00Z") |
561 | +datetime.datetime(2007, 1, 25, 12, 0, tzinfo=<iso8601.iso8601.Utc ...>) |
562 | +>>> |
563 | + |
564 | +Changes |
565 | +======= |
566 | + |
567 | +0.1.4 |
568 | +----- |
569 | + |
570 | +* The default_timezone argument wasn't being passed through correctly, |
571 | + UTC was being used in every case. Fixes issue 10. |
572 | + |
573 | +0.1.3 |
574 | +----- |
575 | + |
576 | +* Fixed the microsecond handling, the generated microsecond values were |
577 | + way too small. Fixes issue 9. |
578 | + |
579 | +0.1.2 |
580 | +----- |
581 | + |
582 | +* Adding ParseError to __all__ in iso8601 module, allows people to import it. |
583 | + Addresses issue 7. |
584 | +* Be a little more flexible when dealing with dates without leading zeroes. |
585 | + This violates the spec a little, but handles more dates as seen in the |
586 | + field. Addresses issue 6. |
587 | +* Allow date/time separators other than T. |
588 | + |
589 | +0.1.1 |
590 | +----- |
591 | + |
592 | +* When parsing dates without a timezone the specified default is used. If no |
593 | + default is specified then UTC is used. Addresses issue 4. |
594 | +""" |
595 | + |
596 | +setup( |
597 | + name="iso8601", |
598 | + version="0.1.4", |
599 | + description=long_description.split("\n")[0], |
600 | + long_description=long_description, |
601 | + author="Michael Twomey", |
602 | + author_email="micktwomey+iso8601@gmail.com", |
603 | + url="http://code.google.com/p/pyiso8601/", |
604 | + packages=["iso8601"], |
605 | + license="MIT", |
606 | +) |
607 | |
608 | === added file 'test/python/iso8601/test_iso8601.py' |
609 | --- test/python/iso8601/test_iso8601.py 1970-01-01 00:00:00 +0000 |
610 | +++ test/python/iso8601/test_iso8601.py 2011-06-10 09:54:56 +0000 |
611 | @@ -0,0 +1,111 @@ |
612 | +import iso8601 |
613 | + |
614 | +def test_iso8601_regex(): |
615 | + assert iso8601.ISO8601_REGEX.match("2006-10-11T00:14:33Z") |
616 | + |
617 | +def test_timezone_regex(): |
618 | + assert iso8601.TIMEZONE_REGEX.match("+01:00") |
619 | + assert iso8601.TIMEZONE_REGEX.match("+00:00") |
620 | + assert iso8601.TIMEZONE_REGEX.match("+01:20") |
621 | + assert iso8601.TIMEZONE_REGEX.match("-01:00") |
622 | + |
623 | +def test_parse_date(): |
624 | + d = iso8601.parse_date("2006-10-20T15:34:56Z") |
625 | + assert d.year == 2006 |
626 | + assert d.month == 10 |
627 | + assert d.day == 20 |
628 | + assert d.hour == 15 |
629 | + assert d.minute == 34 |
630 | + assert d.second == 56 |
631 | + assert d.tzinfo == iso8601.UTC |
632 | + |
633 | +def test_parse_date_fraction(): |
634 | + d = iso8601.parse_date("2006-10-20T15:34:56.123Z") |
635 | + assert d.year == 2006 |
636 | + assert d.month == 10 |
637 | + assert d.day == 20 |
638 | + assert d.hour == 15 |
639 | + assert d.minute == 34 |
640 | + assert d.second == 56 |
641 | + assert d.microsecond == 123000 |
642 | + assert d.tzinfo == iso8601.UTC |
643 | + |
644 | +def test_parse_date_fraction_2(): |
645 | + """From bug 6 |
646 | + |
647 | + """ |
648 | + d = iso8601.parse_date("2007-5-7T11:43:55.328Z'") |
649 | + assert d.year == 2007 |
650 | + assert d.month == 5 |
651 | + assert d.day == 7 |
652 | + assert d.hour == 11 |
653 | + assert d.minute == 43 |
654 | + assert d.second == 55 |
655 | + assert d.microsecond == 328000 |
656 | + assert d.tzinfo == iso8601.UTC |
657 | + |
658 | +def test_parse_date_tz(): |
659 | + d = iso8601.parse_date("2006-10-20T15:34:56.123+02:30") |
660 | + assert d.year == 2006 |
661 | + assert d.month == 10 |
662 | + assert d.day == 20 |
663 | + assert d.hour == 15 |
664 | + assert d.minute == 34 |
665 | + assert d.second == 56 |
666 | + assert d.microsecond == 123000 |
667 | + assert d.tzinfo.tzname(None) == "+02:30" |
668 | + offset = d.tzinfo.utcoffset(None) |
669 | + assert offset.days == 0 |
670 | + assert offset.seconds == 60 * 60 * 2.5 |
671 | + |
672 | +def test_parse_invalid_date(): |
673 | + try: |
674 | + iso8601.parse_date(None) |
675 | + except iso8601.ParseError: |
676 | + pass |
677 | + else: |
678 | + assert 1 == 2 |
679 | + |
680 | +def test_parse_invalid_date2(): |
681 | + try: |
682 | + iso8601.parse_date("23") |
683 | + except iso8601.ParseError: |
684 | + pass |
685 | + else: |
686 | + assert 1 == 2 |
687 | + |
688 | +def test_parse_no_timezone(): |
689 | + """issue 4 - Handle datetime string without timezone |
690 | + |
691 | + This tests what happens when you parse a date with no timezone. While not |
692 | + strictly correct this is quite common. I'll assume UTC for the time zone |
693 | + in this case. |
694 | + """ |
695 | + d = iso8601.parse_date("2007-01-01T08:00:00") |
696 | + assert d.year == 2007 |
697 | + assert d.month == 1 |
698 | + assert d.day == 1 |
699 | + assert d.hour == 8 |
700 | + assert d.minute == 0 |
701 | + assert d.second == 0 |
702 | + assert d.microsecond == 0 |
703 | + assert d.tzinfo == iso8601.UTC |
704 | + |
705 | +def test_parse_no_timezone_different_default(): |
706 | + tz = iso8601.FixedOffset(2, 0, "test offset") |
707 | + d = iso8601.parse_date("2007-01-01T08:00:00", default_timezone=tz) |
708 | + assert d.tzinfo == tz |
709 | + |
710 | +def test_space_separator(): |
711 | + """Handle a separator other than T |
712 | + |
713 | + """ |
714 | + d = iso8601.parse_date("2007-06-23 06:40:34.00Z") |
715 | + assert d.year == 2007 |
716 | + assert d.month == 6 |
717 | + assert d.day == 23 |
718 | + assert d.hour == 6 |
719 | + assert d.minute == 40 |
720 | + assert d.second == 34 |
721 | + assert d.microsecond == 0 |
722 | + assert d.tzinfo == iso8601.UTC |
723 | |
724 | === added directory 'test/python/junitxml' |
725 | === added file 'test/python/junitxml/__init__.py' |
726 | --- test/python/junitxml/__init__.py 1970-01-01 00:00:00 +0000 |
727 | +++ test/python/junitxml/__init__.py 2011-06-10 09:54:56 +0000 |
728 | @@ -0,0 +1,221 @@ |
729 | +# |
730 | +# junitxml: extensions to Python unittest to get output junitxml |
731 | +# Copyright (C) 2009 Robert Collins <robertc@robertcollins.net> |
732 | +# |
733 | +# Copying permitted under the LGPL-3 licence, included with this library. |
734 | + |
735 | +"""unittest compatible JUnit XML output.""" |
736 | + |
737 | + |
738 | +import datetime |
739 | +import re |
740 | +import time |
741 | +import unittest |
742 | + |
743 | +# same format as sys.version_info: "A tuple containing the five components of |
744 | +# the version number: major, minor, micro, releaselevel, and serial. All |
745 | +# values except releaselevel are integers; the release level is 'alpha', |
746 | +# 'beta', 'candidate', or 'final'. The version_info value corresponding to the |
747 | +# Python version 2.0 is (2, 0, 0, 'final', 0)." Additionally we use a |
748 | +# releaselevel of 'dev' for unreleased under-development code. |
749 | +# |
750 | +# If the releaselevel is 'alpha' then the major/minor/micro components are not |
751 | +# established at this point, and setup.py will use a version of next-$(revno). |
752 | +# If the releaselevel is 'final', then the tarball will be major.minor.micro. |
753 | +# Otherwise it is major.minor.micro~$(revno). |
754 | +__version__ = (0, 7, 0, 'alpha', 0) |
755 | + |
756 | + |
757 | +def test_suite(): |
758 | + import junitxml.tests |
759 | + return junitxml.tests.test_suite() |
760 | + |
761 | + |
762 | +class LocalTimezone(datetime.tzinfo): |
763 | + |
764 | + def __init__(self): |
765 | + self._offset = None |
766 | + |
767 | + # It seems that the minimal possible implementation is to just return all |
768 | + # None for every function, but then it breaks... |
769 | + def utcoffset(self, dt): |
770 | + if self._offset is None: |
771 | + t = 1260423030 # arbitrary, but doesn't handle dst very well |
772 | + dt = datetime.datetime |
773 | + self._offset = (dt.fromtimestamp(t) - dt.utcfromtimestamp(t)) |
774 | + return self._offset |
775 | + |
776 | + def dst(self, dt): |
777 | + return datetime.timedelta(0) |
778 | + |
779 | + def tzname(self, dt): |
780 | + return None |
781 | + |
782 | + |
783 | +def _error_name(eclass): |
784 | + module = eclass.__module__ |
785 | + if module not in ("__main__", "builtins", "exceptions"): |
786 | + return ".".join([module, eclass.__name__]) |
787 | + return eclass.__name__ |
788 | + |
789 | + |
790 | +_non_cdata = "[\0-\b\x0B-\x1F\uD800-\uDFFF\uFFFE\uFFFF]+" |
791 | +if "\\u" in _non_cdata: |
792 | + _non_cdata = _non_cdata.decode("unicode-escape") |
793 | + def _strip_invalid_chars(s, _sub=re.compile(_non_cdata, re.UNICODE).sub): |
794 | + if not isinstance(s, unicode): |
795 | + try: |
796 | + s = s.decode("utf-8") |
797 | + except UnicodeDecodeError: |
798 | + s = s.decode("ascii", "replace") |
799 | + return _sub("", s).encode("utf-8") |
800 | +else: |
801 | + def _strip_invalid_chars(s, _sub=re.compile(_non_cdata, re.UNICODE).sub): |
802 | + return _sub("", s) |
803 | +def _escape_content(s): |
804 | + return (_strip_invalid_chars(s) |
805 | + .replace("&", "&") |
806 | + .replace("<", "<") |
807 | + .replace("]]>", "]]>")) |
808 | +def _escape_attr(s): |
809 | + return (_strip_invalid_chars(s) |
810 | + .replace("&", "&") |
811 | + .replace("<", "<") |
812 | + .replace("]]>", "]]>") |
813 | + .replace('"', """) |
814 | + .replace("\t", "	") |
815 | + .replace("\n", "
")) |
816 | + |
817 | + |
818 | +class JUnitXmlResult(unittest.TestResult): |
819 | + """A TestResult which outputs JUnit compatible XML.""" |
820 | + |
821 | + def __init__(self, stream): |
822 | + """Create a JUnitXmlResult. |
823 | + |
824 | + :param stream: A stream to write results to. Note that due to the |
825 | + nature of JUnit XML output, nnothing will be written to the stream |
826 | + until stopTestRun() is called. |
827 | + """ |
828 | + self.__super = super(JUnitXmlResult, self) |
829 | + self.__super.__init__() |
830 | + # GZ 2010-09-03: We have a problem if passed a text stream in Python 3 |
831 | + # as really we want to write raw UTF-8 to ensure that |
832 | + # the encoding is not mangled later |
833 | + self._stream = stream |
834 | + self._results = [] |
835 | + self._set_time = None |
836 | + self._test_start = None |
837 | + self._run_start = None |
838 | + self._tz_info = None |
839 | + |
840 | + def startTestRun(self): |
841 | + """Start a test run.""" |
842 | + self._run_start = self._now() |
843 | + |
844 | + def _get_tzinfo(self): |
845 | + if self._tz_info is None: |
846 | + self._tz_info = LocalTimezone() |
847 | + return self._tz_info |
848 | + |
849 | + def _now(self): |
850 | + if self._set_time is not None: |
851 | + return self._set_time |
852 | + else: |
853 | + return datetime.datetime.now(self._get_tzinfo()) |
854 | + |
855 | + def time(self, a_datetime): |
856 | + self._set_time = a_datetime |
857 | + if (self._run_start is not None and |
858 | + self._run_start > a_datetime): |
859 | + self._run_start = a_datetime |
860 | + |
861 | + def startTest(self, test): |
862 | + self.__super.startTest(test) |
863 | + self._test_start = self._now() |
864 | + |
865 | + def _duration(self, from_datetime): |
866 | + try: |
867 | + delta = self._now() - from_datetime |
868 | + except TypeError: |
869 | + n = self._now() |
870 | + delta = datetime.timedelta(-1) |
871 | + seconds = delta.days * 3600*24 + delta.seconds |
872 | + return seconds + 0.000001 * delta.microseconds |
873 | + |
874 | + def _test_case_string(self, test): |
875 | + duration = self._duration(self._test_start) |
876 | + test_id = test.id() |
877 | + # Split on the last dot not inside a parameter |
878 | + class_end = test_id.rfind(".", 0, test_id.find("(")) |
879 | + if class_end == -1: |
880 | + classname, name = "", test_id |
881 | + else: |
882 | + classname, name = test_id[:class_end], test_id[class_end+1:] |
883 | + self._results.append('<testcase classname="%s" name="%s" ' |
884 | + 'time="%0.3f"' % (_escape_attr(classname), _escape_attr(name), duration)) |
885 | + |
886 | + def stopTestRun(self): |
887 | + """Stop a test run. |
888 | + |
889 | + This allows JUnitXmlResult to output the XML representation of the test |
890 | + run. |
891 | + """ |
892 | + duration = self._duration(self._run_start) |
893 | + self._stream.write('<testsuite errors="%d" failures="%d" name="" ' |
894 | + 'tests="%d" time="%0.3f">\n' % (len(self.errors), |
895 | + len(self.failures) + len(getattr(self, "unexpectedSuccesses", ())), |
896 | + self.testsRun, duration)) |
897 | + self._stream.write(''.join(self._results)) |
898 | + self._stream.write('</testsuite>\n') |
899 | + |
900 | + def addError(self, test, error): |
901 | + self.__super.addError(test, error) |
902 | + self._test_case_string(test) |
903 | + self._results.append('>\n') |
904 | + self._results.append('<error type="%s">%s</error>\n</testcase>\n' % ( |
905 | + _escape_attr(_error_name(error[0])), |
906 | + _escape_content(self._exc_info_to_string(error, test)))) |
907 | + |
908 | + def addFailure(self, test, error): |
909 | + self.__super.addFailure(test, error) |
910 | + self._test_case_string(test) |
911 | + self._results.append('>\n') |
912 | + self._results.append('<failure type="%s">%s</failure>\n</testcase>\n' % |
913 | + (_escape_attr(_error_name(error[0])), |
914 | + _escape_content(self._exc_info_to_string(error, test)))) |
915 | + |
916 | + def addSuccess(self, test): |
917 | + self.__super.addSuccess(test) |
918 | + self._test_case_string(test) |
919 | + self._results.append('/>\n') |
920 | + |
921 | + def addSkip(self, test, reason): |
922 | + try: |
923 | + self.__super.addSkip(test, reason) |
924 | + except AttributeError: |
925 | + # Python < 2.7|3.1 |
926 | + pass |
927 | + self._test_case_string(test) |
928 | + self._results.append('>\n') |
929 | + self._results.append('<skip>%s</skip>\n</testcase>\n'% _escape_attr(reason)) |
930 | + |
931 | + def addUnexpectedSuccess(self, test): |
932 | + try: |
933 | + self.__super.addUnexpectedSuccess(test) |
934 | + except AttributeError: |
935 | + # Python < 2.7|3.1 |
936 | + pass |
937 | + self._test_case_string(test) |
938 | + self._results.append('>\n') |
939 | + self._results.append('<failure type="unittest.case._UnexpectedSuccess"/>\n</testcase>\n') |
940 | + |
941 | + def addExpectedFailure(self, test, error): |
942 | + try: |
943 | + self.__super.addExpectedFailure(test, error) |
944 | + except AttributeError: |
945 | + # Python < 2.7|3.1 |
946 | + pass |
947 | + self._test_case_string(test) |
948 | + self._results.append('/>\n') |
949 | + |
950 | |
951 | === added directory 'test/python/junitxml/tests' |
952 | === added file 'test/python/junitxml/tests/__init__.py' |
953 | --- test/python/junitxml/tests/__init__.py 1970-01-01 00:00:00 +0000 |
954 | +++ test/python/junitxml/tests/__init__.py 2011-06-10 09:54:56 +0000 |
955 | @@ -0,0 +1,16 @@ |
956 | +# |
957 | +# junitxml: extensions to Python unittest to get output junitxml |
958 | +# Copyright (C) 2009 Robert Collins <robertc@robertcollins.net> |
959 | +# |
960 | +# Copying permitted under the LGPL-3 licence, included with this library. |
961 | + |
962 | +import unittest |
963 | + |
964 | +from junitxml.tests import ( |
965 | + test_junitxml, |
966 | + ) |
967 | + |
968 | +def test_suite(): |
969 | + return unittest.TestLoader().loadTestsFromNames([ |
970 | + 'junitxml.tests.test_junitxml', |
971 | + ]) |
972 | |
973 | === added file 'test/python/junitxml/tests/test_junitxml.py' |
974 | --- test/python/junitxml/tests/test_junitxml.py 1970-01-01 00:00:00 +0000 |
975 | +++ test/python/junitxml/tests/test_junitxml.py 2011-06-10 09:54:56 +0000 |
976 | @@ -0,0 +1,327 @@ |
977 | +# |
978 | +# junitxml: extensions to Python unittest to get output junitxml |
979 | +# Copyright (C) 2009 Robert Collins <robertc@robertcollins.net> |
980 | +# |
981 | +# Copying permitted under the LGPL-3 licence, included with this library. |
982 | + |
983 | + |
984 | +try: |
985 | + from cStringIO import StringIO |
986 | +except ImportError: |
987 | + from io import StringIO |
988 | +import datetime |
989 | +import re |
990 | +import sys |
991 | +import unittest |
992 | +import xml.dom.minidom |
993 | + |
994 | +import junitxml |
995 | + |
996 | +class TestImports(unittest.TestCase): |
997 | + |
998 | + def test_result(self): |
999 | + from junitxml import JUnitXmlResult |
1000 | + |
1001 | + |
1002 | +class TestJUnitXmlResult__init__(unittest.TestCase): |
1003 | + |
1004 | + def test_with_stream(self): |
1005 | + result = junitxml.JUnitXmlResult(StringIO()) |
1006 | + |
1007 | + |
1008 | +class TestJUnitXmlResult(unittest.TestCase): |
1009 | + |
1010 | + def setUp(self): |
1011 | + self.output = StringIO() |
1012 | + self.result = junitxml.JUnitXmlResult(self.output) |
1013 | + |
1014 | + def get_output(self): |
1015 | + output = self.output.getvalue() |
1016 | + # Collapse detailed regions into specific strings we can match on |
1017 | + return re.sub(r'(?s)<failure (.*?)>.*?</failure>', |
1018 | + r'<failure \1>failure</failure>', re.sub( |
1019 | + r'(?s)<error (.*?)>.*?</error>', r'<error \1>error</error>', |
1020 | + re.sub(r'time="\d+\.\d+"', 'time="0.000"', output))) |
1021 | + |
1022 | + def run_test_or_simulate(self, test, method_name, manual_method, |
1023 | + *manual_args): |
1024 | + if getattr(test, method_name, None): |
1025 | + test.run(self.result) |
1026 | + else: |
1027 | + # older python - manually execute |
1028 | + self.result.startTest(test) |
1029 | + manual_method(test, *manual_args) |
1030 | + self.result.stopTest(test) |
1031 | + |
1032 | + def test_run_duration_handles_datestamping_in_the_past(self): |
1033 | + # When used via subunit2junitxml, startTestRun is called before |
1034 | + # any tz info in the test stream has been seen. |
1035 | + # So, we use the earliest reported timestamp as the start time, |
1036 | + # replacing _test_start if needed. |
1037 | + self.result.startTestRun() # the time is now. |
1038 | + # Lose an hour (peeks inside, a little naughty but not very). |
1039 | + self.result.time(self.result._run_start - datetime.timedelta(0, 3600)) |
1040 | + self.result.stopTestRun() |
1041 | + self.assertEqual("""<testsuite errors="0" failures="0" name="" tests="0" time="0.000"> |
1042 | +</testsuite> |
1043 | +""", self.get_output()) |
1044 | + |
1045 | + def test_startTestRun_no_output(self): |
1046 | + # startTestRun doesn't output anything, because JUnit wants an up-front |
1047 | + # summary. |
1048 | + self.result.startTestRun() |
1049 | + self.assertEqual('', self.get_output()) |
1050 | + |
1051 | + def test_stopTestRun_outputs(self): |
1052 | + # When stopTestRun is called, everything is output. |
1053 | + self.result.startTestRun() |
1054 | + self.result.stopTestRun() |
1055 | + self.assertEqual("""<testsuite errors="0" failures="0" name="" tests="0" time="0.000"> |
1056 | +</testsuite> |
1057 | +""", self.get_output()) |
1058 | + |
1059 | + def test_test_count(self): |
1060 | + class Passes(unittest.TestCase): |
1061 | + def test_me(self): |
1062 | + pass |
1063 | + self.result.startTestRun() |
1064 | + Passes("test_me").run(self.result) |
1065 | + Passes("test_me").run(self.result) |
1066 | + self.result.stopTestRun() |
1067 | + # When tests are run, the number of tests is counted. |
1068 | + output = self.get_output() |
1069 | + self.assertTrue('tests="2"' in output) |
1070 | + |
1071 | + def test_test_id_with_parameter(self): |
1072 | + class Passes(unittest.TestCase): |
1073 | + def id(self): |
1074 | + return unittest.TestCase.id(self) + '(version_1.6)' |
1075 | + def test_me(self): |
1076 | + pass |
1077 | + self.result.startTestRun() |
1078 | + Passes("test_me").run(self.result) |
1079 | + self.result.stopTestRun() |
1080 | + output = self.get_output() |
1081 | + self.assertTrue('Passes" name="test_me(version_1.6)"' in output) |
1082 | + |
1083 | + def test_erroring_test(self): |
1084 | + class Errors(unittest.TestCase): |
1085 | + def test_me(self): |
1086 | + 1/0 |
1087 | + self.result.startTestRun() |
1088 | + Errors("test_me").run(self.result) |
1089 | + self.result.stopTestRun() |
1090 | + self.assertEqual("""<testsuite errors="1" failures="0" name="" tests="1" time="0.000"> |
1091 | +<testcase classname="junitxml.tests.test_junitxml.Errors" name="test_me" time="0.000"> |
1092 | +<error type="ZeroDivisionError">error</error> |
1093 | +</testcase> |
1094 | +</testsuite> |
1095 | +""", self.get_output()) |
1096 | + |
1097 | + def test_failing_test(self): |
1098 | + class Fails(unittest.TestCase): |
1099 | + def test_me(self): |
1100 | + self.fail() |
1101 | + self.result.startTestRun() |
1102 | + Fails("test_me").run(self.result) |
1103 | + self.result.stopTestRun() |
1104 | + self.assertEqual("""<testsuite errors="0" failures="1" name="" tests="1" time="0.000"> |
1105 | +<testcase classname="junitxml.tests.test_junitxml.Fails" name="test_me" time="0.000"> |
1106 | +<failure type="AssertionError">failure</failure> |
1107 | +</testcase> |
1108 | +</testsuite> |
1109 | +""", self.get_output()) |
1110 | + |
1111 | + def test_successful_test(self): |
1112 | + class Passes(unittest.TestCase): |
1113 | + def test_me(self): |
1114 | + pass |
1115 | + self.result.startTestRun() |
1116 | + Passes("test_me").run(self.result) |
1117 | + self.result.stopTestRun() |
1118 | + self.assertEqual("""<testsuite errors="0" failures="0" name="" tests="1" time="0.000"> |
1119 | +<testcase classname="junitxml.tests.test_junitxml.Passes" name="test_me" time="0.000"/> |
1120 | +</testsuite> |
1121 | +""", self.get_output()) |
1122 | + |
1123 | + def test_skip_test(self): |
1124 | + class Skips(unittest.TestCase): |
1125 | + def test_me(self): |
1126 | + self.skipTest("yo") |
1127 | + self.result.startTestRun() |
1128 | + test = Skips("test_me") |
1129 | + self.run_test_or_simulate(test, 'skipTest', self.result.addSkip, 'yo') |
1130 | + self.result.stopTestRun() |
1131 | + output = self.get_output() |
1132 | + expected = """<testsuite errors="0" failures="0" name="" tests="1" time="0.000"> |
1133 | +<testcase classname="junitxml.tests.test_junitxml.Skips" name="test_me" time="0.000"> |
1134 | +<skip>yo</skip> |
1135 | +</testcase> |
1136 | +</testsuite> |
1137 | +""" |
1138 | + self.assertEqual(expected, output) |
1139 | + |
1140 | + def test_unexpected_success_test(self): |
1141 | + class Succeeds(unittest.TestCase): |
1142 | + def test_me(self): |
1143 | + pass |
1144 | + try: |
1145 | + test_me = unittest.expectedFailure(test_me) |
1146 | + except AttributeError: |
1147 | + pass # Older python - just let the test pass |
1148 | + self.result.startTestRun() |
1149 | + Succeeds("test_me").run(self.result) |
1150 | + self.result.stopTestRun() |
1151 | + output = self.get_output() |
1152 | + expected = """<testsuite errors="0" failures="1" name="" tests="1" time="0.000"> |
1153 | +<testcase classname="junitxml.tests.test_junitxml.Succeeds" name="test_me" time="0.000"> |
1154 | +<failure type="unittest.case._UnexpectedSuccess"/> |
1155 | +</testcase> |
1156 | +</testsuite> |
1157 | +""" |
1158 | + expected_old = """<testsuite errors="0" failures="0" name="" tests="1" time="0.000"> |
1159 | +<testcase classname="junitxml.tests.test_junitxml.Succeeds" name="test_me" time="0.000"/> |
1160 | +</testsuite> |
1161 | +""" |
1162 | + if output != expected_old: |
1163 | + self.assertEqual(expected, output) |
1164 | + |
1165 | + def test_expected_failure_test(self): |
1166 | + expected_failure_support = [True] |
1167 | + class ExpectedFail(unittest.TestCase): |
1168 | + def test_me(self): |
1169 | + self.fail("fail") |
1170 | + try: |
1171 | + test_me = unittest.expectedFailure(test_me) |
1172 | + except AttributeError: |
1173 | + # Older python - just let the test fail |
1174 | + expected_failure_support[0] = False |
1175 | + self.result.startTestRun() |
1176 | + ExpectedFail("test_me").run(self.result) |
1177 | + self.result.stopTestRun() |
1178 | + output = self.get_output() |
1179 | + expected = """<testsuite errors="0" failures="0" name="" tests="1" time="0.000"> |
1180 | +<testcase classname="junitxml.tests.test_junitxml.ExpectedFail" name="test_me" time="0.000"/> |
1181 | +</testsuite> |
1182 | +""" |
1183 | + expected_old = """<testsuite errors="0" failures="1" name="" tests="1" time="0.000"> |
1184 | +<testcase classname="junitxml.tests.test_junitxml.ExpectedFail" name="test_me" time="0.000"> |
1185 | +<failure type="AssertionError">failure</failure> |
1186 | +</testcase> |
1187 | +</testsuite> |
1188 | +""" |
1189 | + if expected_failure_support[0]: |
1190 | + self.assertEqual(expected, output) |
1191 | + else: |
1192 | + self.assertEqual(expected_old, output) |
1193 | + |
1194 | + |
1195 | +class TestWellFormedXml(unittest.TestCase): |
1196 | + """XML created should always be well formed even with odd test cases""" |
1197 | + |
1198 | + def _run_and_parse_test(self, case): |
1199 | + output = StringIO() |
1200 | + result = junitxml.JUnitXmlResult(output) |
1201 | + result.startTestRun() |
1202 | + case.run(result) |
1203 | + result.stopTestRun() |
1204 | + return xml.dom.minidom.parseString(output.getvalue()) |
1205 | + |
1206 | + def test_failure_with_amp(self): |
1207 | + """Check the failure element content is escaped""" |
1208 | + class FailWithAmp(unittest.TestCase): |
1209 | + def runTest(self): |
1210 | + self.fail("& should be escaped as &") |
1211 | + doc = self._run_and_parse_test(FailWithAmp()) |
1212 | + self.assertTrue( |
1213 | + doc.getElementsByTagName("failure")[0].firstChild.nodeValue |
1214 | + .endswith("AssertionError: & should be escaped as &\n")) |
1215 | + |
1216 | + def test_quotes_in_test_case_id(self): |
1217 | + """Check that quotes in an attribute are escaped""" |
1218 | + class QuoteId(unittest.TestCase): |
1219 | + def id(self): |
1220 | + return unittest.TestCase.id(self) + '("quotes")' |
1221 | + def runTest(self): |
1222 | + pass |
1223 | + doc = self._run_and_parse_test(QuoteId()) |
1224 | + self.assertEqual('runTest("quotes")', |
1225 | + doc.getElementsByTagName("testcase")[0].getAttribute("name")) |
1226 | + |
1227 | + def test_skip_reason(self): |
1228 | + """Check the skip element content is escaped""" |
1229 | + class SkipWithLt(unittest.TestCase): |
1230 | + def runTest(self): |
1231 | + self.fail("version < 2.7") |
1232 | + try: |
1233 | + runTest = unittest.skip("2.7 <= version")(runTest) |
1234 | + except AttributeError: |
1235 | + self.has_skip = False |
1236 | + else: |
1237 | + self.has_skip = True |
1238 | + doc = self._run_and_parse_test(SkipWithLt()) |
1239 | + if self.has_skip: |
1240 | + self.assertEqual('2.7 <= version', |
1241 | + doc.getElementsByTagName("skip")[0].firstChild.nodeValue) |
1242 | + else: |
1243 | + self.assertTrue( |
1244 | + doc.getElementsByTagName("failure")[0].firstChild.nodeValue |
1245 | + .endswith("AssertionError: version < 2.7\n")) |
1246 | + |
1247 | + def test_error_with_control_characters(self): |
1248 | + """Check C0 control characters are stripped rather than output""" |
1249 | + class ErrorWithC0(unittest.TestCase): |
1250 | + def runTest(self): |
1251 | + raise ValueError("\x1F\x0E\x0C\x0B\x08\x01\x00lost control") |
1252 | + doc = self._run_and_parse_test(ErrorWithC0()) |
1253 | + self.assertTrue( |
1254 | + doc.getElementsByTagName("error")[0].firstChild.nodeValue |
1255 | + .endswith("ValueError: lost control\n")) |
1256 | + |
1257 | + def test_error_with_invalid_cdata(self): |
1258 | + """Check unicode outside the valid cdata range is stripped""" |
1259 | + if len("\uffff") == 1: |
1260 | + # Basic str type supports unicode |
1261 | + exception = ValueError("\ufffe\uffffEOF") |
1262 | + else: |
1263 | + class UTF8_Error(Exception): |
1264 | + def __unicode__(self): |
1265 | + return str(self).decode("UTF-8") |
1266 | + exception = UTF8_Error("\xef\xbf\xbe\xef\xbf\xbfEOF") |
1267 | + class ErrorWithBadUnicode(unittest.TestCase): |
1268 | + def runTest(self): |
1269 | + raise exception |
1270 | + doc = self._run_and_parse_test(ErrorWithBadUnicode()) |
1271 | + self.assertTrue( |
1272 | + doc.getElementsByTagName("error")[0].firstChild.nodeValue |
1273 | + .endswith("Error: EOF\n")) |
1274 | + |
1275 | + def test_error_with_surrogates(self): |
1276 | + """Check unicode surrogates are handled properly, paired or otherwise |
1277 | + |
1278 | + This is a pain due to suboptimal unicode support in Python and the |
1279 | + various changes in Python 3. On UCS-2 builds there is no easy way of |
1280 | + getting rid of unpaired surrogates while leaving valid pairs alone, so |
1281 | + this test doesn't require astral characters are kept there. |
1282 | + """ |
1283 | + if len("\uffff") == 1: |
1284 | + exception = ValueError("paired: \U000201a2" |
1285 | + " unpaired: "+chr(0xD800)+"-"+chr(0xDFFF)) |
1286 | + astral_char = "\U000201a2" |
1287 | + else: |
1288 | + class UTF8_Error(Exception): |
1289 | + def __unicode__(self): |
1290 | + return str(self).decode("UTF-8") |
1291 | + exception = UTF8_Error("paired: \xf0\xa0\x86\xa2" |
1292 | + " unpaired: \xed\xa0\x80-\xed\xbf\xbf") |
1293 | + astral_char = "\U000201a2".decode("unicode-escape") |
1294 | + class ErrorWithSurrogates(unittest.TestCase): |
1295 | + def runTest(self): |
1296 | + raise exception |
1297 | + doc = self._run_and_parse_test(ErrorWithSurrogates()) |
1298 | + traceback = doc.getElementsByTagName("error")[0].firstChild.nodeValue |
1299 | + if sys.maxunicode == 0xFFFF: |
1300 | + pass # would be nice to handle astral characters properly even so |
1301 | + else: |
1302 | + self.assertTrue(astral_char in traceback) |
1303 | + self.assertTrue(traceback.endswith(" unpaired: -\n")) |
1304 | |
1305 | === added directory 'test/python/subunit' |
1306 | === added file 'test/python/subunit/__init__.py' |
1307 | --- test/python/subunit/__init__.py 1970-01-01 00:00:00 +0000 |
1308 | +++ test/python/subunit/__init__.py 2011-06-10 09:54:56 +0000 |
1309 | @@ -0,0 +1,1250 @@ |
1310 | +# |
1311 | +# subunit: extensions to Python unittest to get test results from subprocesses. |
1312 | +# Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> |
1313 | +# |
1314 | +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause |
1315 | +# license at the users choice. A copy of both licenses are available in the |
1316 | +# project source as Apache-2.0 and BSD. You may not use this file except in |
1317 | +# compliance with one of these two licences. |
1318 | +# |
1319 | +# Unless required by applicable law or agreed to in writing, software |
1320 | +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT |
1321 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
1322 | +# license you chose for the specific language governing permissions and |
1323 | +# limitations under that license. |
1324 | +# |
1325 | + |
1326 | +"""Subunit - a streaming test protocol |
1327 | + |
1328 | +Overview |
1329 | +++++++++ |
1330 | + |
1331 | +The ``subunit`` Python package provides a number of ``unittest`` extensions |
1332 | +which can be used to cause tests to output Subunit, to parse Subunit streams |
1333 | +into test activity, perform seamless test isolation within a regular test |
1334 | +case and variously sort, filter and report on test runs. |
1335 | + |
1336 | + |
1337 | +Key Classes |
1338 | +----------- |
1339 | + |
1340 | +The ``subunit.TestProtocolClient`` class is a ``unittest.TestResult`` |
1341 | +extension which will translate a test run into a Subunit stream. |
1342 | + |
1343 | +The ``subunit.ProtocolTestCase`` class is an adapter between the Subunit wire |
1344 | +protocol and the ``unittest.TestCase`` object protocol. It is used to translate |
1345 | +a stream into a test run, which regular ``unittest.TestResult`` objects can |
1346 | +process and report/inspect. |
1347 | + |
1348 | +Subunit has support for non-blocking usage too, for use with asyncore or |
1349 | +Twisted. See the ``TestProtocolServer`` parser class for more details. |
1350 | + |
1351 | +Subunit includes extensions to the Python ``TestResult`` protocol. These are |
1352 | +all done in a compatible manner: ``TestResult`` objects that do not implement |
1353 | +the extension methods will not cause errors to be raised, instead the extension |
1354 | +will either lose fidelity (for instance, folding expected failures to success |
1355 | +in Python versions < 2.7 or 3.1), or discard the extended data (for extra |
1356 | +details, tags, timestamping and progress markers). |
1357 | + |
1358 | +The test outcome methods ``addSuccess``, ``addError``, ``addExpectedFailure``, |
1359 | +``addFailure``, ``addSkip`` take an optional keyword parameter ``details`` |
1360 | +which can be used instead of the usual python unittest parameter. |
1361 | +When used the value of details should be a dict from ``string`` to |
1362 | +``testtools.content.Content`` objects. This is a draft API being worked on with |
1363 | +the Python Testing In Python mail list, with the goal of permitting a common |
1364 | +way to provide additional data beyond a traceback, such as captured data from |
1365 | +disk, logging messages etc. The reference for this API is in testtools (0.9.0 |
1366 | +and newer). |
1367 | + |
1368 | +The ``tags(new_tags, gone_tags)`` method is called (if present) to add or |
1369 | +remove tags in the test run that is currently executing. If called when no |
1370 | +test is in progress (that is, if called outside of the ``startTest``, |
1371 | +``stopTest`` pair), the the tags apply to all sebsequent tests. If called |
1372 | +when a test is in progress, then the tags only apply to that test. |
1373 | + |
1374 | +The ``time(a_datetime)`` method is called (if present) when a ``time:`` |
1375 | +directive is encountered in a Subunit stream. This is used to tell a TestResult |
1376 | +about the time that events in the stream occured at, to allow reconstructing |
1377 | +test timing from a stream. |
1378 | + |
1379 | +The ``progress(offset, whence)`` method controls progress data for a stream. |
1380 | +The offset parameter is an int, and whence is one of subunit.PROGRESS_CUR, |
1381 | +subunit.PROGRESS_SET, PROGRESS_PUSH, PROGRESS_POP. Push and pop operations |
1382 | +ignore the offset parameter. |
1383 | + |
1384 | + |
1385 | +Python test support |
1386 | +------------------- |
1387 | + |
1388 | +``subunit.run`` is a convenience wrapper to run a Python test suite via |
1389 | +the command line, reporting via Subunit:: |
1390 | + |
1391 | + $ python -m subunit.run mylib.tests.test_suite |
1392 | + |
1393 | +The ``IsolatedTestSuite`` class is a TestSuite that forks before running its |
1394 | +tests, allowing isolation between the test runner and some tests. |
1395 | + |
1396 | +Similarly, ``IsolatedTestCase`` is a base class which can be subclassed to get |
1397 | +tests that will fork() before that individual test is run. |
1398 | + |
1399 | +`ExecTestCase`` is a convenience wrapper for running an external |
1400 | +program to get a Subunit stream and then report that back to an arbitrary |
1401 | +result object:: |
1402 | + |
1403 | + class AggregateTests(subunit.ExecTestCase): |
1404 | + |
1405 | + def test_script_one(self): |
1406 | + './bin/script_one' |
1407 | + |
1408 | + def test_script_two(self): |
1409 | + './bin/script_two' |
1410 | + |
1411 | + # Normally your normal test loading would take of this automatically, |
1412 | + # It is only spelt out in detail here for clarity. |
1413 | + suite = unittest.TestSuite([AggregateTests("test_script_one"), |
1414 | + AggregateTests("test_script_two")]) |
1415 | + # Create any TestResult class you like. |
1416 | + result = unittest._TextTestResult(sys.stdout) |
1417 | + # And run your suite as normal, Subunit will exec each external script as |
1418 | + # needed and report to your result object. |
1419 | + suite.run(result) |
1420 | + |
1421 | +Utility modules |
1422 | +--------------- |
1423 | + |
1424 | +* subunit.chunked contains HTTP chunked encoding/decoding logic. |
1425 | +* subunit.test_results contains TestResult helper classes. |
1426 | +""" |
1427 | + |
1428 | +import os |
1429 | +import re |
1430 | +import subprocess |
1431 | +import sys |
1432 | +import unittest |
1433 | + |
1434 | +from testtools import content, content_type, ExtendedToOriginalDecorator |
1435 | +from testtools.compat import _b, _u, BytesIO, StringIO |
1436 | +try: |
1437 | + from testtools.testresult.real import _StringException |
1438 | + RemoteException = _StringException |
1439 | + # For testing: different pythons have different str() implementations. |
1440 | + if sys.version_info > (3, 0): |
1441 | + _remote_exception_str = "testtools.testresult.real._StringException" |
1442 | + _remote_exception_str_chunked = "34\r\n" + _remote_exception_str |
1443 | + else: |
1444 | + _remote_exception_str = "_StringException" |
1445 | + _remote_exception_str_chunked = "1A\r\n" + _remote_exception_str |
1446 | +except ImportError: |
1447 | + raise ImportError ("testtools.testresult.real does not contain " |
1448 | + "_StringException, check your version.") |
1449 | +from testtools import testresult |
1450 | + |
1451 | +from subunit import chunked, details, iso8601, test_results |
1452 | + |
1453 | + |
1454 | +PROGRESS_SET = 0 |
1455 | +PROGRESS_CUR = 1 |
1456 | +PROGRESS_PUSH = 2 |
1457 | +PROGRESS_POP = 3 |
1458 | + |
1459 | + |
1460 | +def test_suite(): |
1461 | + import subunit.tests |
1462 | + return subunit.tests.test_suite() |
1463 | + |
1464 | + |
1465 | +def join_dir(base_path, path): |
1466 | + """ |
1467 | + Returns an absolute path to C{path}, calculated relative to the parent |
1468 | + of C{base_path}. |
1469 | + |
1470 | + @param base_path: A path to a file or directory. |
1471 | + @param path: An absolute path, or a path relative to the containing |
1472 | + directory of C{base_path}. |
1473 | + |
1474 | + @return: An absolute path to C{path}. |
1475 | + """ |
1476 | + return os.path.join(os.path.dirname(os.path.abspath(base_path)), path) |
1477 | + |
1478 | + |
1479 | +def tags_to_new_gone(tags): |
1480 | + """Split a list of tags into a new_set and a gone_set.""" |
1481 | + new_tags = set() |
1482 | + gone_tags = set() |
1483 | + for tag in tags: |
1484 | + if tag[0] == '-': |
1485 | + gone_tags.add(tag[1:]) |
1486 | + else: |
1487 | + new_tags.add(tag) |
1488 | + return new_tags, gone_tags |
1489 | + |
1490 | + |
1491 | +class DiscardStream(object): |
1492 | + """A filelike object which discards what is written to it.""" |
1493 | + |
1494 | + def write(self, bytes): |
1495 | + pass |
1496 | + |
1497 | + |
1498 | +class _ParserState(object): |
1499 | + """State for the subunit parser.""" |
1500 | + |
1501 | + def __init__(self, parser): |
1502 | + self.parser = parser |
1503 | + self._test_sym = (_b('test'), _b('testing')) |
1504 | + self._colon_sym = _b(':') |
1505 | + self._error_sym = (_b('error'),) |
1506 | + self._failure_sym = (_b('failure'),) |
1507 | + self._progress_sym = (_b('progress'),) |
1508 | + self._skip_sym = _b('skip') |
1509 | + self._success_sym = (_b('success'), _b('successful')) |
1510 | + self._tags_sym = (_b('tags'),) |
1511 | + self._time_sym = (_b('time'),) |
1512 | + self._xfail_sym = (_b('xfail'),) |
1513 | + self._uxsuccess_sym = (_b('uxsuccess'),) |
1514 | + self._start_simple = _u(" [") |
1515 | + self._start_multipart = _u(" [ multipart") |
1516 | + |
1517 | + def addError(self, offset, line): |
1518 | + """An 'error:' directive has been read.""" |
1519 | + self.parser.stdOutLineReceived(line) |
1520 | + |
1521 | + def addExpectedFail(self, offset, line): |
1522 | + """An 'xfail:' directive has been read.""" |
1523 | + self.parser.stdOutLineReceived(line) |
1524 | + |
1525 | + def addFailure(self, offset, line): |
1526 | + """A 'failure:' directive has been read.""" |
1527 | + self.parser.stdOutLineReceived(line) |
1528 | + |
1529 | + def addSkip(self, offset, line): |
1530 | + """A 'skip:' directive has been read.""" |
1531 | + self.parser.stdOutLineReceived(line) |
1532 | + |
1533 | + def addSuccess(self, offset, line): |
1534 | + """A 'success:' directive has been read.""" |
1535 | + self.parser.stdOutLineReceived(line) |
1536 | + |
1537 | + def lineReceived(self, line): |
1538 | + """a line has been received.""" |
1539 | + parts = line.split(None, 1) |
1540 | + if len(parts) == 2 and line.startswith(parts[0]): |
1541 | + cmd, rest = parts |
1542 | + offset = len(cmd) + 1 |
1543 | + cmd = cmd.rstrip(self._colon_sym) |
1544 | + if cmd in self._test_sym: |
1545 | + self.startTest(offset, line) |
1546 | + elif cmd in self._error_sym: |
1547 | + self.addError(offset, line) |
1548 | + elif cmd in self._failure_sym: |
1549 | + self.addFailure(offset, line) |
1550 | + elif cmd in self._progress_sym: |
1551 | + self.parser._handleProgress(offset, line) |
1552 | + elif cmd in self._skip_sym: |
1553 | + self.addSkip(offset, line) |
1554 | + elif cmd in self._success_sym: |
1555 | + self.addSuccess(offset, line) |
1556 | + elif cmd in self._tags_sym: |
1557 | + self.parser._handleTags(offset, line) |
1558 | + self.parser.subunitLineReceived(line) |
1559 | + elif cmd in self._time_sym: |
1560 | + self.parser._handleTime(offset, line) |
1561 | + self.parser.subunitLineReceived(line) |
1562 | + elif cmd in self._xfail_sym: |
1563 | + self.addExpectedFail(offset, line) |
1564 | + elif cmd in self._uxsuccess_sym: |
1565 | + self.addUnexpectedSuccess(offset, line) |
1566 | + else: |
1567 | + self.parser.stdOutLineReceived(line) |
1568 | + else: |
1569 | + self.parser.stdOutLineReceived(line) |
1570 | + |
1571 | + def lostConnection(self): |
1572 | + """Connection lost.""" |
1573 | + self.parser._lostConnectionInTest(_u('unknown state of ')) |
1574 | + |
1575 | + def startTest(self, offset, line): |
1576 | + """A test start command received.""" |
1577 | + self.parser.stdOutLineReceived(line) |
1578 | + |
1579 | + |
1580 | +class _InTest(_ParserState): |
1581 | + """State for the subunit parser after reading a test: directive.""" |
1582 | + |
1583 | + def _outcome(self, offset, line, no_details, details_state): |
1584 | + """An outcome directive has been read. |
1585 | + |
1586 | + :param no_details: Callable to call when no details are presented. |
1587 | + :param details_state: The state to switch to for details |
1588 | + processing of this outcome. |
1589 | + """ |
1590 | + test_name = line[offset:-1].decode('utf8') |
1591 | + if self.parser.current_test_description == test_name: |
1592 | + self.parser._state = self.parser._outside_test |
1593 | + self.parser.current_test_description = None |
1594 | + no_details() |
1595 | + self.parser.client.stopTest(self.parser._current_test) |
1596 | + self.parser._current_test = None |
1597 | + self.parser.subunitLineReceived(line) |
1598 | + elif self.parser.current_test_description + self._start_simple == \ |
1599 | + test_name: |
1600 | + self.parser._state = details_state |
1601 | + details_state.set_simple() |
1602 | + self.parser.subunitLineReceived(line) |
1603 | + elif self.parser.current_test_description + self._start_multipart == \ |
1604 | + test_name: |
1605 | + self.parser._state = details_state |
1606 | + details_state.set_multipart() |
1607 | + self.parser.subunitLineReceived(line) |
1608 | + else: |
1609 | + self.parser.stdOutLineReceived(line) |
1610 | + |
1611 | + def _error(self): |
1612 | + self.parser.client.addError(self.parser._current_test, |
1613 | + details={}) |
1614 | + |
1615 | + def addError(self, offset, line): |
1616 | + """An 'error:' directive has been read.""" |
1617 | + self._outcome(offset, line, self._error, |
1618 | + self.parser._reading_error_details) |
1619 | + |
1620 | + def _xfail(self): |
1621 | + self.parser.client.addExpectedFailure(self.parser._current_test, |
1622 | + details={}) |
1623 | + |
1624 | + def addExpectedFail(self, offset, line): |
1625 | + """An 'xfail:' directive has been read.""" |
1626 | + self._outcome(offset, line, self._xfail, |
1627 | + self.parser._reading_xfail_details) |
1628 | + |
1629 | + def _uxsuccess(self): |
1630 | + self.parser.client.addUnexpectedSuccess(self.parser._current_test) |
1631 | + |
1632 | + def addUnexpectedSuccess(self, offset, line): |
1633 | + """A 'uxsuccess:' directive has been read.""" |
1634 | + self._outcome(offset, line, self._uxsuccess, |
1635 | + self.parser._reading_uxsuccess_details) |
1636 | + |
1637 | + def _failure(self): |
1638 | + self.parser.client.addFailure(self.parser._current_test, details={}) |
1639 | + |
1640 | + def addFailure(self, offset, line): |
1641 | + """A 'failure:' directive has been read.""" |
1642 | + self._outcome(offset, line, self._failure, |
1643 | + self.parser._reading_failure_details) |
1644 | + |
1645 | + def _skip(self): |
1646 | + self.parser.client.addSkip(self.parser._current_test, details={}) |
1647 | + |
1648 | + def addSkip(self, offset, line): |
1649 | + """A 'skip:' directive has been read.""" |
1650 | + self._outcome(offset, line, self._skip, |
1651 | + self.parser._reading_skip_details) |
1652 | + |
1653 | + def _succeed(self): |
1654 | + self.parser.client.addSuccess(self.parser._current_test, details={}) |
1655 | + |
1656 | + def addSuccess(self, offset, line): |
1657 | + """A 'success:' directive has been read.""" |
1658 | + self._outcome(offset, line, self._succeed, |
1659 | + self.parser._reading_success_details) |
1660 | + |
1661 | + def lostConnection(self): |
1662 | + """Connection lost.""" |
1663 | + self.parser._lostConnectionInTest(_u('')) |
1664 | + |
1665 | + |
1666 | +class _OutSideTest(_ParserState): |
1667 | + """State for the subunit parser outside of a test context.""" |
1668 | + |
1669 | + def lostConnection(self): |
1670 | + """Connection lost.""" |
1671 | + |
1672 | + def startTest(self, offset, line): |
1673 | + """A test start command received.""" |
1674 | + self.parser._state = self.parser._in_test |
1675 | + test_name = line[offset:-1].decode('utf8') |
1676 | + self.parser._current_test = RemotedTestCase(test_name) |
1677 | + self.parser.current_test_description = test_name |
1678 | + self.parser.client.startTest(self.parser._current_test) |
1679 | + self.parser.subunitLineReceived(line) |
1680 | + |
1681 | + |
1682 | +class _ReadingDetails(_ParserState): |
1683 | + """Common logic for readin state details.""" |
1684 | + |
1685 | + def endDetails(self): |
1686 | + """The end of a details section has been reached.""" |
1687 | + self.parser._state = self.parser._outside_test |
1688 | + self.parser.current_test_description = None |
1689 | + self._report_outcome() |
1690 | + self.parser.client.stopTest(self.parser._current_test) |
1691 | + |
1692 | + def lineReceived(self, line): |
1693 | + """a line has been received.""" |
1694 | + self.details_parser.lineReceived(line) |
1695 | + self.parser.subunitLineReceived(line) |
1696 | + |
1697 | + def lostConnection(self): |
1698 | + """Connection lost.""" |
1699 | + self.parser._lostConnectionInTest(_u('%s report of ') % |
1700 | + self._outcome_label()) |
1701 | + |
1702 | + def _outcome_label(self): |
1703 | + """The label to describe this outcome.""" |
1704 | + raise NotImplementedError(self._outcome_label) |
1705 | + |
1706 | + def set_simple(self): |
1707 | + """Start a simple details parser.""" |
1708 | + self.details_parser = details.SimpleDetailsParser(self) |
1709 | + |
1710 | + def set_multipart(self): |
1711 | + """Start a multipart details parser.""" |
1712 | + self.details_parser = details.MultipartDetailsParser(self) |
1713 | + |
1714 | + |
1715 | +class _ReadingFailureDetails(_ReadingDetails): |
1716 | + """State for the subunit parser when reading failure details.""" |
1717 | + |
1718 | + def _report_outcome(self): |
1719 | + self.parser.client.addFailure(self.parser._current_test, |
1720 | + details=self.details_parser.get_details()) |
1721 | + |
1722 | + def _outcome_label(self): |
1723 | + return "failure" |
1724 | + |
1725 | + |
1726 | +class _ReadingErrorDetails(_ReadingDetails): |
1727 | + """State for the subunit parser when reading error details.""" |
1728 | + |
1729 | + def _report_outcome(self): |
1730 | + self.parser.client.addError(self.parser._current_test, |
1731 | + details=self.details_parser.get_details()) |
1732 | + |
1733 | + def _outcome_label(self): |
1734 | + return "error" |
1735 | + |
1736 | + |
1737 | +class _ReadingExpectedFailureDetails(_ReadingDetails): |
1738 | + """State for the subunit parser when reading xfail details.""" |
1739 | + |
1740 | + def _report_outcome(self): |
1741 | + self.parser.client.addExpectedFailure(self.parser._current_test, |
1742 | + details=self.details_parser.get_details()) |
1743 | + |
1744 | + def _outcome_label(self): |
1745 | + return "xfail" |
1746 | + |
1747 | + |
1748 | +class _ReadingUnexpectedSuccessDetails(_ReadingDetails): |
1749 | + """State for the subunit parser when reading uxsuccess details.""" |
1750 | + |
1751 | + def _report_outcome(self): |
1752 | + self.parser.client.addUnexpectedSuccess(self.parser._current_test, |
1753 | + details=self.details_parser.get_details()) |
1754 | + |
1755 | + def _outcome_label(self): |
1756 | + return "uxsuccess" |
1757 | + |
1758 | + |
1759 | +class _ReadingSkipDetails(_ReadingDetails): |
1760 | + """State for the subunit parser when reading skip details.""" |
1761 | + |
1762 | + def _report_outcome(self): |
1763 | + self.parser.client.addSkip(self.parser._current_test, |
1764 | + details=self.details_parser.get_details("skip")) |
1765 | + |
1766 | + def _outcome_label(self): |
1767 | + return "skip" |
1768 | + |
1769 | + |
1770 | +class _ReadingSuccessDetails(_ReadingDetails): |
1771 | + """State for the subunit parser when reading success details.""" |
1772 | + |
1773 | + def _report_outcome(self): |
1774 | + self.parser.client.addSuccess(self.parser._current_test, |
1775 | + details=self.details_parser.get_details("success")) |
1776 | + |
1777 | + def _outcome_label(self): |
1778 | + return "success" |
1779 | + |
1780 | + |
1781 | +class TestProtocolServer(object): |
1782 | + """A parser for subunit. |
1783 | + |
1784 | + :ivar tags: The current tags associated with the protocol stream. |
1785 | + """ |
1786 | + |
1787 | + def __init__(self, client, stream=None, forward_stream=None): |
1788 | + """Create a TestProtocolServer instance. |
1789 | + |
1790 | + :param client: An object meeting the unittest.TestResult protocol. |
1791 | + :param stream: The stream that lines received which are not part of the |
1792 | + subunit protocol should be written to. This allows custom handling |
1793 | + of mixed protocols. By default, sys.stdout will be used for |
1794 | + convenience. It should accept bytes to its write() method. |
1795 | + :param forward_stream: A stream to forward subunit lines to. This |
1796 | + allows a filter to forward the entire stream while still parsing |
1797 | + and acting on it. By default forward_stream is set to |
1798 | + DiscardStream() and no forwarding happens. |
1799 | + """ |
1800 | + self.client = ExtendedToOriginalDecorator(client) |
1801 | + if stream is None: |
1802 | + stream = sys.stdout |
1803 | + if sys.version_info > (3, 0): |
1804 | + stream = stream.buffer |
1805 | + self._stream = stream |
1806 | + self._forward_stream = forward_stream or DiscardStream() |
1807 | + # state objects we can switch too |
1808 | + self._in_test = _InTest(self) |
1809 | + self._outside_test = _OutSideTest(self) |
1810 | + self._reading_error_details = _ReadingErrorDetails(self) |
1811 | + self._reading_failure_details = _ReadingFailureDetails(self) |
1812 | + self._reading_skip_details = _ReadingSkipDetails(self) |
1813 | + self._reading_success_details = _ReadingSuccessDetails(self) |
1814 | + self._reading_xfail_details = _ReadingExpectedFailureDetails(self) |
1815 | + self._reading_uxsuccess_details = _ReadingUnexpectedSuccessDetails(self) |
1816 | + # start with outside test. |
1817 | + self._state = self._outside_test |
1818 | + # Avoid casts on every call |
1819 | + self._plusminus = _b('+-') |
1820 | + self._push_sym = _b('push') |
1821 | + self._pop_sym = _b('pop') |
1822 | + |
1823 | + def _handleProgress(self, offset, line): |
1824 | + """Process a progress directive.""" |
1825 | + line = line[offset:].strip() |
1826 | + if line[0] in self._plusminus: |
1827 | + whence = PROGRESS_CUR |
1828 | + delta = int(line) |
1829 | + elif line == self._push_sym: |
1830 | + whence = PROGRESS_PUSH |
1831 | + delta = None |
1832 | + elif line == self._pop_sym: |
1833 | + whence = PROGRESS_POP |
1834 | + delta = None |
1835 | + else: |
1836 | + whence = PROGRESS_SET |
1837 | + delta = int(line) |
1838 | + self.client.progress(delta, whence) |
1839 | + |
1840 | + def _handleTags(self, offset, line): |
1841 | + """Process a tags command.""" |
1842 | + tags = line[offset:].decode('utf8').split() |
1843 | + new_tags, gone_tags = tags_to_new_gone(tags) |
1844 | + self.client.tags(new_tags, gone_tags) |
1845 | + |
1846 | + def _handleTime(self, offset, line): |
1847 | + # Accept it, but do not do anything with it yet. |
1848 | + try: |
1849 | + event_time = iso8601.parse_date(line[offset:-1]) |
1850 | + except TypeError: |
1851 | + raise TypeError(_u("Failed to parse %r, got %r") |
1852 | + % (line, sys.exec_info[1])) |
1853 | + self.client.time(event_time) |
1854 | + |
1855 | + def lineReceived(self, line): |
1856 | + """Call the appropriate local method for the received line.""" |
1857 | + self._state.lineReceived(line) |
1858 | + |
1859 | + def _lostConnectionInTest(self, state_string): |
1860 | + error_string = _u("lost connection during %stest '%s'") % ( |
1861 | + state_string, self.current_test_description) |
1862 | + self.client.addError(self._current_test, RemoteError(error_string)) |
1863 | + self.client.stopTest(self._current_test) |
1864 | + |
1865 | + def lostConnection(self): |
1866 | + """The input connection has finished.""" |
1867 | + self._state.lostConnection() |
1868 | + |
1869 | + def readFrom(self, pipe): |
1870 | + """Blocking convenience API to parse an entire stream. |
1871 | + |
1872 | + :param pipe: A file-like object supporting readlines(). |
1873 | + :return: None. |
1874 | + """ |
1875 | + for line in pipe.readlines(): |
1876 | + self.lineReceived(line) |
1877 | + self.lostConnection() |
1878 | + |
1879 | + def _startTest(self, offset, line): |
1880 | + """Internal call to change state machine. Override startTest().""" |
1881 | + self._state.startTest(offset, line) |
1882 | + |
1883 | + def subunitLineReceived(self, line): |
1884 | + self._forward_stream.write(line) |
1885 | + |
1886 | + def stdOutLineReceived(self, line): |
1887 | + self._stream.write(line) |
1888 | + |
1889 | + |
1890 | +class TestProtocolClient(testresult.TestResult): |
1891 | + """A TestResult which generates a subunit stream for a test run. |
1892 | + |
1893 | + # Get a TestSuite or TestCase to run |
1894 | + suite = make_suite() |
1895 | + # Create a stream (any object with a 'write' method). This should accept |
1896 | + # bytes not strings: subunit is a byte orientated protocol. |
1897 | + stream = file('tests.log', 'wb') |
1898 | + # Create a subunit result object which will output to the stream |
1899 | + result = subunit.TestProtocolClient(stream) |
1900 | + # Optionally, to get timing data for performance analysis, wrap the |
1901 | + # serialiser with a timing decorator |
1902 | + result = subunit.test_results.AutoTimingTestResultDecorator(result) |
1903 | + # Run the test suite reporting to the subunit result object |
1904 | + suite.run(result) |
1905 | + # Close the stream. |
1906 | + stream.close() |
1907 | + """ |
1908 | + |
1909 | + def __init__(self, stream): |
1910 | + testresult.TestResult.__init__(self) |
1911 | + self._stream = stream |
1912 | + _make_stream_binary(stream) |
1913 | + self._progress_fmt = _b("progress: ") |
1914 | + self._bytes_eol = _b("\n") |
1915 | + self._progress_plus = _b("+") |
1916 | + self._progress_push = _b("push") |
1917 | + self._progress_pop = _b("pop") |
1918 | + self._empty_bytes = _b("") |
1919 | + self._start_simple = _b(" [\n") |
1920 | + self._end_simple = _b("]\n") |
1921 | + |
1922 | + def addError(self, test, error=None, details=None): |
1923 | + """Report an error in test test. |
1924 | + |
1925 | + Only one of error and details should be provided: conceptually there |
1926 | + are two separate methods: |
1927 | + addError(self, test, error) |
1928 | + addError(self, test, details) |
1929 | + |
1930 | + :param error: Standard unittest positional argument form - an |
1931 | + exc_info tuple. |
1932 | + :param details: New Testing-in-python drafted API; a dict from string |
1933 | + to subunit.Content objects. |
1934 | + """ |
1935 | + self._addOutcome("error", test, error=error, details=details) |
1936 | + |
1937 | + def addExpectedFailure(self, test, error=None, details=None): |
1938 | + """Report an expected failure in test test. |
1939 | + |
1940 | + Only one of error and details should be provided: conceptually there |
1941 | + are two separate methods: |
1942 | + addError(self, test, error) |
1943 | + addError(self, test, details) |
1944 | + |
1945 | + :param error: Standard unittest positional argument form - an |
1946 | + exc_info tuple. |
1947 | + :param details: New Testing-in-python drafted API; a dict from string |
1948 | + to subunit.Content objects. |
1949 | + """ |
1950 | + self._addOutcome("xfail", test, error=error, details=details) |
1951 | + |
1952 | + def addFailure(self, test, error=None, details=None): |
1953 | + """Report a failure in test test. |
1954 | + |
1955 | + Only one of error and details should be provided: conceptually there |
1956 | + are two separate methods: |
1957 | + addFailure(self, test, error) |
1958 | + addFailure(self, test, details) |
1959 | + |
1960 | + :param error: Standard unittest positional argument form - an |
1961 | + exc_info tuple. |
1962 | + :param details: New Testing-in-python drafted API; a dict from string |
1963 | + to subunit.Content objects. |
1964 | + """ |
1965 | + self._addOutcome("failure", test, error=error, details=details) |
1966 | + |
1967 | + def _addOutcome(self, outcome, test, error=None, details=None, |
1968 | + error_permitted=True): |
1969 | + """Report a failure in test test. |
1970 | + |
1971 | + Only one of error and details should be provided: conceptually there |
1972 | + are two separate methods: |
1973 | + addOutcome(self, test, error) |
1974 | + addOutcome(self, test, details) |
1975 | + |
1976 | + :param outcome: A string describing the outcome - used as the |
1977 | + event name in the subunit stream. |
1978 | + :param error: Standard unittest positional argument form - an |
1979 | + exc_info tuple. |
1980 | + :param details: New Testing-in-python drafted API; a dict from string |
1981 | + to subunit.Content objects. |
1982 | + :param error_permitted: If True then one and only one of error or |
1983 | + details must be supplied. If False then error must not be supplied |
1984 | + and details is still optional. """ |
1985 | + self._stream.write(_b("%s: %s" % (outcome, test.id()))) |
1986 | + if error_permitted: |
1987 | + if error is None and details is None: |
1988 | + raise ValueError |
1989 | + else: |
1990 | + if error is not None: |
1991 | + raise ValueError |
1992 | + if error is not None: |
1993 | + self._stream.write(self._start_simple) |
1994 | + # XXX: this needs to be made much stricter, along the lines of |
1995 | + # Martin[gz]'s work in testtools. Perhaps subunit can use that? |
1996 | + for line in self._exc_info_to_unicode(error, test).splitlines(): |
1997 | + self._stream.write(("%s\n" % line).encode('utf8')) |
1998 | + elif details is not None: |
1999 | + self._write_details(details) |
2000 | + else: |
2001 | + self._stream.write(_b("\n")) |
2002 | + if details is not None or error is not None: |
2003 | + self._stream.write(self._end_simple) |
2004 | + |
2005 | + def addSkip(self, test, reason=None, details=None): |
2006 | + """Report a skipped test.""" |
2007 | + if reason is None: |
2008 | + self._addOutcome("skip", test, error=None, details=details) |
2009 | + else: |
2010 | + self._stream.write(_b("skip: %s [\n" % test.id())) |
2011 | + self._stream.write(_b("%s\n" % reason)) |
2012 | + self._stream.write(self._end_simple) |
2013 | + |
2014 | + def addSuccess(self, test, details=None): |
2015 | + """Report a success in a test.""" |
2016 | + self._addOutcome("successful", test, details=details, error_permitted=False) |
2017 | + |
2018 | + def addUnexpectedSuccess(self, test, details=None): |
2019 | + """Report an unexpected success in test test. |
2020 | + |
2021 | + Details can optionally be provided: conceptually there |
2022 | + are two separate methods: |
2023 | + addError(self, test) |
2024 | + addError(self, test, details) |
2025 | + |
2026 | + :param details: New Testing-in-python drafted API; a dict from string |
2027 | + to subunit.Content objects. |
2028 | + """ |
2029 | + self._addOutcome("uxsuccess", test, details=details, |
2030 | + error_permitted=False) |
2031 | + |
2032 | + def startTest(self, test): |
2033 | + """Mark a test as starting its test run.""" |
2034 | + super(TestProtocolClient, self).startTest(test) |
2035 | + self._stream.write(_b("test: %s\n" % test.id())) |
2036 | + self._stream.flush() |
2037 | + |
2038 | + def stopTest(self, test): |
2039 | + super(TestProtocolClient, self).stopTest(test) |
2040 | + self._stream.flush() |
2041 | + |
2042 | + def progress(self, offset, whence): |
2043 | + """Provide indication about the progress/length of the test run. |
2044 | + |
2045 | + :param offset: Information about the number of tests remaining. If |
2046 | + whence is PROGRESS_CUR, then offset increases/decreases the |
2047 | + remaining test count. If whence is PROGRESS_SET, then offset |
2048 | + specifies exactly the remaining test count. |
2049 | + :param whence: One of PROGRESS_CUR, PROGRESS_SET, PROGRESS_PUSH, |
2050 | + PROGRESS_POP. |
2051 | + """ |
2052 | + if whence == PROGRESS_CUR and offset > -1: |
2053 | + prefix = self._progress_plus |
2054 | + offset = _b(str(offset)) |
2055 | + elif whence == PROGRESS_PUSH: |
2056 | + prefix = self._empty_bytes |
2057 | + offset = self._progress_push |
2058 | + elif whence == PROGRESS_POP: |
2059 | + prefix = self._empty_bytes |
2060 | + offset = self._progress_pop |
2061 | + else: |
2062 | + prefix = self._empty_bytes |
2063 | + offset = _b(str(offset)) |
2064 | + self._stream.write(self._progress_fmt + prefix + offset + |
2065 | + self._bytes_eol) |
2066 | + |
2067 | + def time(self, a_datetime): |
2068 | + """Inform the client of the time. |
2069 | + |
2070 | + ":param datetime: A datetime.datetime object. |
2071 | + """ |
2072 | + time = a_datetime.astimezone(iso8601.Utc()) |
2073 | + self._stream.write(_b("time: %04d-%02d-%02d %02d:%02d:%02d.%06dZ\n" % ( |
2074 | + time.year, time.month, time.day, time.hour, time.minute, |
2075 | + time.second, time.microsecond))) |
2076 | + |
2077 | + def _write_details(self, details): |
2078 | + """Output details to the stream. |
2079 | + |
2080 | + :param details: An extended details dict for a test outcome. |
2081 | + """ |
2082 | + self._stream.write(_b(" [ multipart\n")) |
2083 | + for name, content in sorted(details.items()): |
2084 | + self._stream.write(_b("Content-Type: %s/%s" % |
2085 | + (content.content_type.type, content.content_type.subtype))) |
2086 | + parameters = content.content_type.parameters |
2087 | + if parameters: |
2088 | + self._stream.write(_b(";")) |
2089 | + param_strs = [] |
2090 | + for param, value in parameters.items(): |
2091 | + param_strs.append("%s=%s" % (param, value)) |
2092 | + self._stream.write(_b(",".join(param_strs))) |
2093 | + self._stream.write(_b("\n%s\n" % name)) |
2094 | + encoder = chunked.Encoder(self._stream) |
2095 | + list(map(encoder.write, content.iter_bytes())) |
2096 | + encoder.close() |
2097 | + |
2098 | + def done(self): |
2099 | + """Obey the testtools result.done() interface.""" |
2100 | + |
2101 | + |
2102 | +def RemoteError(description=_u("")): |
2103 | + return (_StringException, _StringException(description), None) |
2104 | + |
2105 | + |
2106 | +class RemotedTestCase(unittest.TestCase): |
2107 | + """A class to represent test cases run in child processes. |
2108 | + |
2109 | + Instances of this class are used to provide the Python test API a TestCase |
2110 | + that can be printed to the screen, introspected for metadata and so on. |
2111 | + However, as they are a simply a memoisation of a test that was actually |
2112 | + run in the past by a separate process, they cannot perform any interactive |
2113 | + actions. |
2114 | + """ |
2115 | + |
2116 | + def __eq__ (self, other): |
2117 | + try: |
2118 | + return self.__description == other.__description |
2119 | + except AttributeError: |
2120 | + return False |
2121 | + |
2122 | + def __init__(self, description): |
2123 | + """Create a psuedo test case with description description.""" |
2124 | + self.__description = description |
2125 | + |
2126 | + def error(self, label): |
2127 | + raise NotImplementedError("%s on RemotedTestCases is not permitted." % |
2128 | + label) |
2129 | + |
2130 | + def setUp(self): |
2131 | + self.error("setUp") |
2132 | + |
2133 | + def tearDown(self): |
2134 | + self.error("tearDown") |
2135 | + |
2136 | + def shortDescription(self): |
2137 | + return self.__description |
2138 | + |
2139 | + def id(self): |
2140 | + return "%s" % (self.__description,) |
2141 | + |
2142 | + def __str__(self): |
2143 | + return "%s (%s)" % (self.__description, self._strclass()) |
2144 | + |
2145 | + def __repr__(self): |
2146 | + return "<%s description='%s'>" % \ |
2147 | + (self._strclass(), self.__description) |
2148 | + |
2149 | + def run(self, result=None): |
2150 | + if result is None: result = self.defaultTestResult() |
2151 | + result.startTest(self) |
2152 | + result.addError(self, RemoteError(_u("Cannot run RemotedTestCases.\n"))) |
2153 | + result.stopTest(self) |
2154 | + |
2155 | + def _strclass(self): |
2156 | + cls = self.__class__ |
2157 | + return "%s.%s" % (cls.__module__, cls.__name__) |
2158 | + |
2159 | + |
2160 | +class ExecTestCase(unittest.TestCase): |
2161 | + """A test case which runs external scripts for test fixtures.""" |
2162 | + |
2163 | + def __init__(self, methodName='runTest'): |
2164 | + """Create an instance of the class that will use the named test |
2165 | + method when executed. Raises a ValueError if the instance does |
2166 | + not have a method with the specified name. |
2167 | + """ |
2168 | + unittest.TestCase.__init__(self, methodName) |
2169 | + testMethod = getattr(self, methodName) |
2170 | + self.script = join_dir(sys.modules[self.__class__.__module__].__file__, |
2171 | + testMethod.__doc__) |
2172 | + |
2173 | + def countTestCases(self): |
2174 | + return 1 |
2175 | + |
2176 | + def run(self, result=None): |
2177 | + if result is None: result = self.defaultTestResult() |
2178 | + self._run(result) |
2179 | + |
2180 | + def debug(self): |
2181 | + """Run the test without collecting errors in a TestResult""" |
2182 | + self._run(testresult.TestResult()) |
2183 | + |
2184 | + def _run(self, result): |
2185 | + protocol = TestProtocolServer(result) |
2186 | + process = subprocess.Popen(self.script, shell=True, |
2187 | + stdout=subprocess.PIPE) |
2188 | + _make_stream_binary(process.stdout) |
2189 | + output = process.communicate()[0] |
2190 | + protocol.readFrom(BytesIO(output)) |
2191 | + |
2192 | + |
2193 | +class IsolatedTestCase(unittest.TestCase): |
2194 | + """A TestCase which executes in a forked process. |
2195 | + |
2196 | + Each test gets its own process, which has a performance overhead but will |
2197 | + provide excellent isolation from global state (such as django configs, |
2198 | + zope utilities and so on). |
2199 | + """ |
2200 | + |
2201 | + def run(self, result=None): |
2202 | + if result is None: result = self.defaultTestResult() |
2203 | + run_isolated(unittest.TestCase, self, result) |
2204 | + |
2205 | + |
2206 | +class IsolatedTestSuite(unittest.TestSuite): |
2207 | + """A TestSuite which runs its tests in a forked process. |
2208 | + |
2209 | + This decorator that will fork() before running the tests and report the |
2210 | + results from the child process using a Subunit stream. This is useful for |
2211 | + handling tests that mutate global state, or are testing C extensions that |
2212 | + could crash the VM. |
2213 | + """ |
2214 | + |
2215 | + def run(self, result=None): |
2216 | + if result is None: result = testresult.TestResult() |
2217 | + run_isolated(unittest.TestSuite, self, result) |
2218 | + |
2219 | + |
2220 | +def run_isolated(klass, self, result): |
2221 | + """Run a test suite or case in a subprocess, using the run method on klass. |
2222 | + """ |
2223 | + c2pread, c2pwrite = os.pipe() |
2224 | + # fixme - error -> result |
2225 | + # now fork |
2226 | + pid = os.fork() |
2227 | + if pid == 0: |
2228 | + # Child |
2229 | + # Close parent's pipe ends |
2230 | + os.close(c2pread) |
2231 | + # Dup fds for child |
2232 | + os.dup2(c2pwrite, 1) |
2233 | + # Close pipe fds. |
2234 | + os.close(c2pwrite) |
2235 | + |
2236 | + # at this point, sys.stdin is redirected, now we want |
2237 | + # to filter it to escape ]'s. |
2238 | + ### XXX: test and write that bit. |
2239 | + stream = os.fdopen(1, 'wb') |
2240 | + result = TestProtocolClient(stream) |
2241 | + klass.run(self, result) |
2242 | + stream.flush() |
2243 | + sys.stderr.flush() |
2244 | + # exit HARD, exit NOW. |
2245 | + os._exit(0) |
2246 | + else: |
2247 | + # Parent |
2248 | + # Close child pipe ends |
2249 | + os.close(c2pwrite) |
2250 | + # hookup a protocol engine |
2251 | + protocol = TestProtocolServer(result) |
2252 | + fileobj = os.fdopen(c2pread, 'rb') |
2253 | + protocol.readFrom(fileobj) |
2254 | + os.waitpid(pid, 0) |
2255 | + # TODO return code evaluation. |
2256 | + return result |
2257 | + |
2258 | + |
2259 | +def TAP2SubUnit(tap, subunit): |
2260 | + """Filter a TAP pipe into a subunit pipe. |
2261 | + |
2262 | + :param tap: A tap pipe/stream/file object. |
2263 | + :param subunit: A pipe/stream/file object to write subunit results to. |
2264 | + :return: The exit code to exit with. |
2265 | + """ |
2266 | + BEFORE_PLAN = 0 |
2267 | + AFTER_PLAN = 1 |
2268 | + SKIP_STREAM = 2 |
2269 | + state = BEFORE_PLAN |
2270 | + plan_start = 1 |
2271 | + plan_stop = 0 |
2272 | + def _skipped_test(subunit, plan_start): |
2273 | + # Some tests were skipped. |
2274 | + subunit.write('test test %d\n' % plan_start) |
2275 | + subunit.write('error test %d [\n' % plan_start) |
2276 | + subunit.write('test missing from TAP output\n') |
2277 | + subunit.write(']\n') |
2278 | + return plan_start + 1 |
2279 | + # Test data for the next test to emit |
2280 | + test_name = None |
2281 | + log = [] |
2282 | + result = None |
2283 | + def _emit_test(): |
2284 | + "write out a test" |
2285 | + if test_name is None: |
2286 | + return |
2287 | + subunit.write("test %s\n" % test_name) |
2288 | + if not log: |
2289 | + subunit.write("%s %s\n" % (result, test_name)) |
2290 | + else: |
2291 | + subunit.write("%s %s [\n" % (result, test_name)) |
2292 | + if log: |
2293 | + for line in log: |
2294 | + subunit.write("%s\n" % line) |
2295 | + subunit.write("]\n") |
2296 | + del log[:] |
2297 | + for line in tap: |
2298 | + if state == BEFORE_PLAN: |
2299 | + match = re.match("(\d+)\.\.(\d+)\s*(?:\#\s+(.*))?\n", line) |
2300 | + if match: |
2301 | + state = AFTER_PLAN |
2302 | + _, plan_stop, comment = match.groups() |
2303 | + plan_stop = int(plan_stop) |
2304 | + if plan_start > plan_stop and plan_stop == 0: |
2305 | + # skipped file |
2306 | + state = SKIP_STREAM |
2307 | + subunit.write("test file skip\n") |
2308 | + subunit.write("skip file skip [\n") |
2309 | + subunit.write("%s\n" % comment) |
2310 | + subunit.write("]\n") |
2311 | + continue |
2312 | + # not a plan line, or have seen one before |
2313 | + match = re.match("(ok|not ok)(?:\s+(\d+)?)?(?:\s+([^#]*[^#\s]+)\s*)?(?:\s+#\s+(TODO|SKIP|skip|todo)(?:\s+(.*))?)?\n", line) |
2314 | + if match: |
2315 | + # new test, emit current one. |
2316 | + _emit_test() |
2317 | + status, number, description, directive, directive_comment = match.groups() |
2318 | + if status == 'ok': |
2319 | + result = 'success' |
2320 | + else: |
2321 | + result = "failure" |
2322 | + if description is None: |
2323 | + description = '' |
2324 | + else: |
2325 | + description = ' ' + description |
2326 | + if directive is not None: |
2327 | + if directive.upper() == 'TODO': |
2328 | + result = 'xfail' |
2329 | + elif directive.upper() == 'SKIP': |
2330 | + result = 'skip' |
2331 | + if directive_comment is not None: |
2332 | + log.append(directive_comment) |
2333 | + if number is not None: |
2334 | + number = int(number) |
2335 | + while plan_start < number: |
2336 | + plan_start = _skipped_test(subunit, plan_start) |
2337 | + test_name = "test %d%s" % (plan_start, description) |
2338 | + plan_start += 1 |
2339 | + continue |
2340 | + match = re.match("Bail out\!(?:\s*(.*))?\n", line) |
2341 | + if match: |
2342 | + reason, = match.groups() |
2343 | + if reason is None: |
2344 | + extra = '' |
2345 | + else: |
2346 | + extra = ' %s' % reason |
2347 | + _emit_test() |
2348 | + test_name = "Bail out!%s" % extra |
2349 | + result = "error" |
2350 | + state = SKIP_STREAM |
2351 | + continue |
2352 | + match = re.match("\#.*\n", line) |
2353 | + if match: |
2354 | + log.append(line[:-1]) |
2355 | + continue |
2356 | + subunit.write(line) |
2357 | + _emit_test() |
2358 | + while plan_start <= plan_stop: |
2359 | + # record missed tests |
2360 | + plan_start = _skipped_test(subunit, plan_start) |
2361 | + return 0 |
2362 | + |
2363 | + |
2364 | +def tag_stream(original, filtered, tags): |
2365 | + """Alter tags on a stream. |
2366 | + |
2367 | + :param original: The input stream. |
2368 | + :param filtered: The output stream. |
2369 | + :param tags: The tags to apply. As in a normal stream - a list of 'TAG' or |
2370 | + '-TAG' commands. |
2371 | + |
2372 | + A 'TAG' command will add the tag to the output stream, |
2373 | + and override any existing '-TAG' command in that stream. |
2374 | + Specifically: |
2375 | + * A global 'tags: TAG' will be added to the start of the stream. |
2376 | + * Any tags commands with -TAG will have the -TAG removed. |
2377 | + |
2378 | + A '-TAG' command will remove the TAG command from the stream. |
2379 | + Specifically: |
2380 | + * A 'tags: -TAG' command will be added to the start of the stream. |
2381 | + * Any 'tags: TAG' command will have 'TAG' removed from it. |
2382 | + Additionally, any redundant tagging commands (adding a tag globally |
2383 | + present, or removing a tag globally removed) are stripped as a |
2384 | + by-product of the filtering. |
2385 | + :return: 0 |
2386 | + """ |
2387 | + new_tags, gone_tags = tags_to_new_gone(tags) |
2388 | + def write_tags(new_tags, gone_tags): |
2389 | + if new_tags or gone_tags: |
2390 | + filtered.write("tags: " + ' '.join(new_tags)) |
2391 | + if gone_tags: |
2392 | + for tag in gone_tags: |
2393 | + filtered.write("-" + tag) |
2394 | + filtered.write("\n") |
2395 | + write_tags(new_tags, gone_tags) |
2396 | + # TODO: use the protocol parser and thus don't mangle test comments. |
2397 | + for line in original: |
2398 | + if line.startswith("tags:"): |
2399 | + line_tags = line[5:].split() |
2400 | + line_new, line_gone = tags_to_new_gone(line_tags) |
2401 | + line_new = line_new - gone_tags |
2402 | + line_gone = line_gone - new_tags |
2403 | + write_tags(line_new, line_gone) |
2404 | + else: |
2405 | + filtered.write(line) |
2406 | + return 0 |
2407 | + |
2408 | + |
2409 | +class ProtocolTestCase(object): |
2410 | + """Subunit wire protocol to unittest.TestCase adapter. |
2411 | + |
2412 | + ProtocolTestCase honours the core of ``unittest.TestCase`` protocol - |
2413 | + calling a ProtocolTestCase or invoking the run() method will make a 'test |
2414 | + run' happen. The 'test run' will simply be a replay of the test activity |
2415 | + that has been encoded into the stream. The ``unittest.TestCase`` ``debug`` |
2416 | + and ``countTestCases`` methods are not supported because there isn't a |
2417 | + sensible mapping for those methods. |
2418 | + |
2419 | + # Get a stream (any object with a readline() method), in this case the |
2420 | + # stream output by the example from ``subunit.TestProtocolClient``. |
2421 | + stream = file('tests.log', 'rb') |
2422 | + # Create a parser which will read from the stream and emit |
2423 | + # activity to a unittest.TestResult when run() is called. |
2424 | + suite = subunit.ProtocolTestCase(stream) |
2425 | + # Create a result object to accept the contents of that stream. |
2426 | + result = unittest._TextTestResult(sys.stdout) |
2427 | + # 'run' the tests - process the stream and feed its contents to result. |
2428 | + suite.run(result) |
2429 | + stream.close() |
2430 | + |
2431 | + :seealso: TestProtocolServer (the subunit wire protocol parser). |
2432 | + """ |
2433 | + |
2434 | + def __init__(self, stream, passthrough=None, forward=False): |
2435 | + """Create a ProtocolTestCase reading from stream. |
2436 | + |
2437 | + :param stream: A filelike object which a subunit stream can be read |
2438 | + from. |
2439 | + :param passthrough: A stream pass non subunit input on to. If not |
2440 | + supplied, the TestProtocolServer default is used. |
2441 | + :param forward: A stream to pass subunit input on to. If not supplied |
2442 | + subunit input is not forwarded. |
2443 | + """ |
2444 | + self._stream = stream |
2445 | + _make_stream_binary(stream) |
2446 | + self._passthrough = passthrough |
2447 | + self._forward = forward |
2448 | + |
2449 | + def __call__(self, result=None): |
2450 | + return self.run(result) |
2451 | + |
2452 | + def run(self, result=None): |
2453 | + if result is None: |
2454 | + result = self.defaultTestResult() |
2455 | + protocol = TestProtocolServer(result, self._passthrough, self._forward) |
2456 | + line = self._stream.readline() |
2457 | + while line: |
2458 | + protocol.lineReceived(line) |
2459 | + line = self._stream.readline() |
2460 | + protocol.lostConnection() |
2461 | + |
2462 | + |
2463 | +class TestResultStats(testresult.TestResult): |
2464 | + """A pyunit TestResult interface implementation for making statistics. |
2465 | + |
2466 | + :ivar total_tests: The total tests seen. |
2467 | + :ivar passed_tests: The tests that passed. |
2468 | + :ivar failed_tests: The tests that failed. |
2469 | + :ivar seen_tags: The tags seen across all tests. |
2470 | + """ |
2471 | + |
2472 | + def __init__(self, stream): |
2473 | + """Create a TestResultStats which outputs to stream.""" |
2474 | + testresult.TestResult.__init__(self) |
2475 | + self._stream = stream |
2476 | + self.failed_tests = 0 |
2477 | + self.skipped_tests = 0 |
2478 | + self.seen_tags = set() |
2479 | + |
2480 | + @property |
2481 | + def total_tests(self): |
2482 | + return self.testsRun |
2483 | + |
2484 | + def addError(self, test, err, details=None): |
2485 | + self.failed_tests += 1 |
2486 | + |
2487 | + def addFailure(self, test, err, details=None): |
2488 | + self.failed_tests += 1 |
2489 | + |
2490 | + def addSkip(self, test, reason, details=None): |
2491 | + self.skipped_tests += 1 |
2492 | + |
2493 | + def formatStats(self): |
2494 | + self._stream.write("Total tests: %5d\n" % self.total_tests) |
2495 | + self._stream.write("Passed tests: %5d\n" % self.passed_tests) |
2496 | + self._stream.write("Failed tests: %5d\n" % self.failed_tests) |
2497 | + self._stream.write("Skipped tests: %5d\n" % self.skipped_tests) |
2498 | + tags = sorted(self.seen_tags) |
2499 | + self._stream.write("Seen tags: %s\n" % (", ".join(tags))) |
2500 | + |
2501 | + @property |
2502 | + def passed_tests(self): |
2503 | + return self.total_tests - self.failed_tests - self.skipped_tests |
2504 | + |
2505 | + def tags(self, new_tags, gone_tags): |
2506 | + """Accumulate the seen tags.""" |
2507 | + self.seen_tags.update(new_tags) |
2508 | + |
2509 | + def wasSuccessful(self): |
2510 | + """Tells whether or not this result was a success""" |
2511 | + return self.failed_tests == 0 |
2512 | + |
2513 | + |
2514 | +def get_default_formatter(): |
2515 | + """Obtain the default formatter to write to. |
2516 | + |
2517 | + :return: A file-like object. |
2518 | + """ |
2519 | + formatter = os.getenv("SUBUNIT_FORMATTER") |
2520 | + if formatter: |
2521 | + return os.popen(formatter, "w") |
2522 | + else: |
2523 | + stream = sys.stdout |
2524 | + if sys.version_info > (3, 0): |
2525 | + stream = stream.buffer |
2526 | + return stream |
2527 | + |
2528 | + |
2529 | +if sys.version_info > (3, 0): |
2530 | + from io import UnsupportedOperation as _NoFilenoError |
2531 | +else: |
2532 | + _NoFilenoError = AttributeError |
2533 | + |
2534 | +def read_test_list(path): |
2535 | + """Read a list of test ids from a file on disk. |
2536 | + |
2537 | + :param path: Path to the file |
2538 | + :return: Sequence of test ids |
2539 | + """ |
2540 | + f = open(path, 'rb') |
2541 | + try: |
2542 | + return [l.rstrip("\n") for l in f.readlines()] |
2543 | + finally: |
2544 | + f.close() |
2545 | + |
2546 | + |
2547 | +def _make_stream_binary(stream): |
2548 | + """Ensure that a stream will be binary safe. See _make_binary_on_windows.""" |
2549 | + try: |
2550 | + fileno = stream.fileno() |
2551 | + except _NoFilenoError: |
2552 | + return |
2553 | + _make_binary_on_windows(fileno) |
2554 | + |
2555 | +def _make_binary_on_windows(fileno): |
2556 | + """Win32 mangles \r\n to \n and that breaks streams. See bug lp:505078.""" |
2557 | + if sys.platform == "win32": |
2558 | + import msvcrt |
2559 | + msvcrt.setmode(fileno, os.O_BINARY) |
2560 | |
2561 | === added file 'test/python/subunit/chunked.py' |
2562 | --- test/python/subunit/chunked.py 1970-01-01 00:00:00 +0000 |
2563 | +++ test/python/subunit/chunked.py 2011-06-10 09:54:56 +0000 |
2564 | @@ -0,0 +1,185 @@ |
2565 | +# |
2566 | +# subunit: extensions to python unittest to get test results from subprocesses. |
2567 | +# Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> |
2568 | +# Copyright (C) 2011 Martin Pool <mbp@sourcefrog.net> |
2569 | +# |
2570 | +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause |
2571 | +# license at the users choice. A copy of both licenses are available in the |
2572 | +# project source as Apache-2.0 and BSD. You may not use this file except in |
2573 | +# compliance with one of these two licences. |
2574 | +# |
2575 | +# Unless required by applicable law or agreed to in writing, software |
2576 | +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT |
2577 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
2578 | +# license you chose for the specific language governing permissions and |
2579 | +# limitations under that license. |
2580 | +# |
2581 | + |
2582 | +"""Encoder/decoder for http style chunked encoding.""" |
2583 | + |
2584 | +from testtools.compat import _b |
2585 | + |
2586 | +empty = _b('') |
2587 | + |
2588 | +class Decoder(object): |
2589 | + """Decode chunked content to a byte stream.""" |
2590 | + |
2591 | + def __init__(self, output, strict=True): |
2592 | + """Create a decoder decoding to output. |
2593 | + |
2594 | + :param output: A file-like object. Bytes written to the Decoder are |
2595 | + decoded to strip off the chunking and written to the output. |
2596 | + Up to a full write worth of data or a single control line may be |
2597 | + buffered (whichever is larger). The close method should be called |
2598 | + when no more data is available, to detect short streams; the |
2599 | + write method will return none-None when the end of a stream is |
2600 | + detected. The output object must accept bytes objects. |
2601 | + |
2602 | + :param strict: If True (the default), the decoder will not knowingly |
2603 | + accept input that is not conformant to the HTTP specification. |
2604 | + (This does not imply that it will catch every nonconformance.) |
2605 | + If False, it will accept incorrect input that is still |
2606 | + unambiguous. |
2607 | + """ |
2608 | + self.output = output |
2609 | + self.buffered_bytes = [] |
2610 | + self.state = self._read_length |
2611 | + self.body_length = 0 |
2612 | + self.strict = strict |
2613 | + self._match_chars = _b("0123456789abcdefABCDEF\r\n") |
2614 | + self._slash_n = _b('\n') |
2615 | + self._slash_r = _b('\r') |
2616 | + self._slash_rn = _b('\r\n') |
2617 | + self._slash_nr = _b('\n\r') |
2618 | + |
2619 | + def close(self): |
2620 | + """Close the decoder. |
2621 | + |
2622 | + :raises ValueError: If the stream is incomplete ValueError is raised. |
2623 | + """ |
2624 | + if self.state != self._finished: |
2625 | + raise ValueError("incomplete stream") |
2626 | + |
2627 | + def _finished(self): |
2628 | + """Finished reading, return any remaining bytes.""" |
2629 | + if self.buffered_bytes: |
2630 | + buffered_bytes = self.buffered_bytes |
2631 | + self.buffered_bytes = [] |
2632 | + return empty.join(buffered_bytes) |
2633 | + else: |
2634 | + raise ValueError("stream is finished") |
2635 | + |
2636 | + def _read_body(self): |
2637 | + """Pass body bytes to the output.""" |
2638 | + while self.body_length and self.buffered_bytes: |
2639 | + if self.body_length >= len(self.buffered_bytes[0]): |
2640 | + self.output.write(self.buffered_bytes[0]) |
2641 | + self.body_length -= len(self.buffered_bytes[0]) |
2642 | + del self.buffered_bytes[0] |
2643 | + # No more data available. |
2644 | + if not self.body_length: |
2645 | + self.state = self._read_length |
2646 | + else: |
2647 | + self.output.write(self.buffered_bytes[0][:self.body_length]) |
2648 | + self.buffered_bytes[0] = \ |
2649 | + self.buffered_bytes[0][self.body_length:] |
2650 | + self.body_length = 0 |
2651 | + self.state = self._read_length |
2652 | + return self.state() |
2653 | + |
2654 | + def _read_length(self): |
2655 | + """Try to decode a length from the bytes.""" |
2656 | + count_chars = [] |
2657 | + for bytes in self.buffered_bytes: |
2658 | + for pos in range(len(bytes)): |
2659 | + byte = bytes[pos:pos+1] |
2660 | + if byte not in self._match_chars: |
2661 | + break |
2662 | + count_chars.append(byte) |
2663 | + if byte == self._slash_n: |
2664 | + break |
2665 | + if not count_chars: |
2666 | + return |
2667 | + if count_chars[-1] != self._slash_n: |
2668 | + return |
2669 | + count_str = empty.join(count_chars) |
2670 | + if self.strict: |
2671 | + if count_str[-2:] != self._slash_rn: |
2672 | + raise ValueError("chunk header invalid: %r" % count_str) |
2673 | + if self._slash_r in count_str[:-2]: |
2674 | + raise ValueError("too many CRs in chunk header %r" % count_str) |
2675 | + self.body_length = int(count_str.rstrip(self._slash_nr), 16) |
2676 | + excess_bytes = len(count_str) |
2677 | + while excess_bytes: |
2678 | + if excess_bytes >= len(self.buffered_bytes[0]): |
2679 | + excess_bytes -= len(self.buffered_bytes[0]) |
2680 | + del self.buffered_bytes[0] |
2681 | + else: |
2682 | + self.buffered_bytes[0] = self.buffered_bytes[0][excess_bytes:] |
2683 | + excess_bytes = 0 |
2684 | + if not self.body_length: |
2685 | + self.state = self._finished |
2686 | + if not self.buffered_bytes: |
2687 | + # May not call into self._finished with no buffered data. |
2688 | + return empty |
2689 | + else: |
2690 | + self.state = self._read_body |
2691 | + return self.state() |
2692 | + |
2693 | + def write(self, bytes): |
2694 | + """Decode bytes to the output stream. |
2695 | + |
2696 | + :raises ValueError: If the stream has already seen the end of file |
2697 | + marker. |
2698 | + :returns: None, or the excess bytes beyond the end of file marker. |
2699 | + """ |
2700 | + if bytes: |
2701 | + self.buffered_bytes.append(bytes) |
2702 | + return self.state() |
2703 | + |
2704 | + |
2705 | +class Encoder(object): |
2706 | + """Encode content to a stream using HTTP Chunked coding.""" |
2707 | + |
2708 | + def __init__(self, output): |
2709 | + """Create an encoder encoding to output. |
2710 | + |
2711 | + :param output: A file-like object. Bytes written to the Encoder |
2712 | + will be encoded using HTTP chunking. Small writes may be buffered |
2713 | + and the ``close`` method must be called to finish the stream. |
2714 | + """ |
2715 | + self.output = output |
2716 | + self.buffered_bytes = [] |
2717 | + self.buffer_size = 0 |
2718 | + |
2719 | + def flush(self, extra_len=0): |
2720 | + """Flush the encoder to the output stream. |
2721 | + |
2722 | + :param extra_len: Increase the size of the chunk by this many bytes |
2723 | + to allow for a subsequent write. |
2724 | + """ |
2725 | + if not self.buffer_size and not extra_len: |
2726 | + return |
2727 | + buffered_bytes = self.buffered_bytes |
2728 | + buffer_size = self.buffer_size |
2729 | + self.buffered_bytes = [] |
2730 | + self.buffer_size = 0 |
2731 | + self.output.write(_b("%X\r\n" % (buffer_size + extra_len))) |
2732 | + if buffer_size: |
2733 | + self.output.write(empty.join(buffered_bytes)) |
2734 | + return True |
2735 | + |
2736 | + def write(self, bytes): |
2737 | + """Encode bytes to the output stream.""" |
2738 | + bytes_len = len(bytes) |
2739 | + if self.buffer_size + bytes_len >= 65536: |
2740 | + self.flush(bytes_len) |
2741 | + self.output.write(bytes) |
2742 | + else: |
2743 | + self.buffered_bytes.append(bytes) |
2744 | + self.buffer_size += bytes_len |
2745 | + |
2746 | + def close(self): |
2747 | + """Finish the stream. This does not close the output stream.""" |
2748 | + self.flush() |
2749 | + self.output.write(_b("0\r\n")) |
2750 | |
2751 | === added file 'test/python/subunit/details.py' |
2752 | --- test/python/subunit/details.py 1970-01-01 00:00:00 +0000 |
2753 | +++ test/python/subunit/details.py 2011-06-10 09:54:56 +0000 |
2754 | @@ -0,0 +1,119 @@ |
2755 | +# |
2756 | +# subunit: extensions to Python unittest to get test results from subprocesses. |
2757 | +# Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> |
2758 | +# |
2759 | +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause |
2760 | +# license at the users choice. A copy of both licenses are available in the |
2761 | +# project source as Apache-2.0 and BSD. You may not use this file except in |
2762 | +# compliance with one of these two licences. |
2763 | +# |
2764 | +# Unless required by applicable law or agreed to in writing, software |
2765 | +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT |
2766 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
2767 | +# license you chose for the specific language governing permissions and |
2768 | +# limitations under that license. |
2769 | +# |
2770 | + |
2771 | +"""Handlers for outcome details.""" |
2772 | + |
2773 | +from testtools import content, content_type |
2774 | +from testtools.compat import _b, BytesIO |
2775 | + |
2776 | +from subunit import chunked |
2777 | + |
2778 | +end_marker = _b("]\n") |
2779 | +quoted_marker = _b(" ]") |
2780 | +empty = _b('') |
2781 | + |
2782 | + |
2783 | +class DetailsParser(object): |
2784 | + """Base class/API reference for details parsing.""" |
2785 | + |
2786 | + |
2787 | +class SimpleDetailsParser(DetailsParser): |
2788 | + """Parser for single-part [] delimited details.""" |
2789 | + |
2790 | + def __init__(self, state): |
2791 | + self._message = _b("") |
2792 | + self._state = state |
2793 | + |
2794 | + def lineReceived(self, line): |
2795 | + if line == end_marker: |
2796 | + self._state.endDetails() |
2797 | + return |
2798 | + if line[0:2] == quoted_marker: |
2799 | + # quoted ] start |
2800 | + self._message += line[1:] |
2801 | + else: |
2802 | + self._message += line |
2803 | + |
2804 | + def get_details(self, style=None): |
2805 | + result = {} |
2806 | + if not style: |
2807 | + # We know that subunit/testtools serialise [] formatted |
2808 | + # tracebacks as utf8, but perhaps we need a ReplacingContent |
2809 | + # or something like that. |
2810 | + result['traceback'] = content.Content( |
2811 | + content_type.ContentType("text", "x-traceback", |
2812 | + {"charset": "utf8"}), |
2813 | + lambda:[self._message]) |
2814 | + else: |
2815 | + if style == 'skip': |
2816 | + name = 'reason' |
2817 | + else: |
2818 | + name = 'message' |
2819 | + result[name] = content.Content( |
2820 | + content_type.ContentType("text", "plain"), |
2821 | + lambda:[self._message]) |
2822 | + return result |
2823 | + |
2824 | + def get_message(self): |
2825 | + return self._message |
2826 | + |
2827 | + |
2828 | +class MultipartDetailsParser(DetailsParser): |
2829 | + """Parser for multi-part [] surrounded MIME typed chunked details.""" |
2830 | + |
2831 | + def __init__(self, state): |
2832 | + self._state = state |
2833 | + self._details = {} |
2834 | + self._parse_state = self._look_for_content |
2835 | + |
2836 | + def _look_for_content(self, line): |
2837 | + if line == end_marker: |
2838 | + self._state.endDetails() |
2839 | + return |
2840 | + # TODO error handling |
2841 | + field, value = line[:-1].decode('utf8').split(' ', 1) |
2842 | + try: |
2843 | + main, sub = value.split('/') |
2844 | + except ValueError: |
2845 | + raise ValueError("Invalid MIME type %r" % value) |
2846 | + self._content_type = content_type.ContentType(main, sub) |
2847 | + self._parse_state = self._get_name |
2848 | + |
2849 | + def _get_name(self, line): |
2850 | + self._name = line[:-1].decode('utf8') |
2851 | + self._body = BytesIO() |
2852 | + self._chunk_parser = chunked.Decoder(self._body) |
2853 | + self._parse_state = self._feed_chunks |
2854 | + |
2855 | + def _feed_chunks(self, line): |
2856 | + residue = self._chunk_parser.write(line) |
2857 | + if residue is not None: |
2858 | + # Line based use always ends on no residue. |
2859 | + assert residue == empty, 'residue: %r' % (residue,) |
2860 | + body = self._body |
2861 | + self._details[self._name] = content.Content( |
2862 | + self._content_type, lambda:[body.getvalue()]) |
2863 | + self._chunk_parser.close() |
2864 | + self._parse_state = self._look_for_content |
2865 | + |
2866 | + def get_details(self, for_skip=False): |
2867 | + return self._details |
2868 | + |
2869 | + def get_message(self): |
2870 | + return None |
2871 | + |
2872 | + def lineReceived(self, line): |
2873 | + self._parse_state(line) |
2874 | |
2875 | === added file 'test/python/subunit/iso8601.py' |
2876 | --- test/python/subunit/iso8601.py 1970-01-01 00:00:00 +0000 |
2877 | +++ test/python/subunit/iso8601.py 2011-06-10 09:54:56 +0000 |
2878 | @@ -0,0 +1,133 @@ |
2879 | +# Copyright (c) 2007 Michael Twomey |
2880 | +# |
2881 | +# Permission is hereby granted, free of charge, to any person obtaining a |
2882 | +# copy of this software and associated documentation files (the |
2883 | +# "Software"), to deal in the Software without restriction, including |
2884 | +# without limitation the rights to use, copy, modify, merge, publish, |
2885 | +# distribute, sublicense, and/or sell copies of the Software, and to |
2886 | +# permit persons to whom the Software is furnished to do so, subject to |
2887 | +# the following conditions: |
2888 | +# |
2889 | +# The above copyright notice and this permission notice shall be included |
2890 | +# in all copies or substantial portions of the Software. |
2891 | +# |
2892 | +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
2893 | +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
2894 | +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. |
2895 | +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY |
2896 | +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, |
2897 | +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE |
2898 | +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
2899 | + |
2900 | +"""ISO 8601 date time string parsing |
2901 | + |
2902 | +Basic usage: |
2903 | +>>> import iso8601 |
2904 | +>>> iso8601.parse_date("2007-01-25T12:00:00Z") |
2905 | +datetime.datetime(2007, 1, 25, 12, 0, tzinfo=<iso8601.iso8601.Utc ...>) |
2906 | +>>> |
2907 | + |
2908 | +""" |
2909 | + |
2910 | +from datetime import datetime, timedelta, tzinfo |
2911 | +import re |
2912 | +import sys |
2913 | + |
2914 | +__all__ = ["parse_date", "ParseError"] |
2915 | + |
2916 | +# Adapted from http://delete.me.uk/2005/03/iso8601.html |
2917 | +ISO8601_REGEX_PATTERN = (r"(?P<year>[0-9]{4})(-(?P<month>[0-9]{1,2})(-(?P<day>[0-9]{1,2})" |
2918 | + r"((?P<separator>.)(?P<hour>[0-9]{2}):(?P<minute>[0-9]{2})(:(?P<second>[0-9]{2})(\.(?P<fraction>[0-9]+))?)?" |
2919 | + r"(?P<timezone>Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?" |
2920 | +) |
2921 | +TIMEZONE_REGEX_PATTERN = "(?P<prefix>[+-])(?P<hours>[0-9]{2}).(?P<minutes>[0-9]{2})" |
2922 | +ISO8601_REGEX = re.compile(ISO8601_REGEX_PATTERN.encode('utf8')) |
2923 | +TIMEZONE_REGEX = re.compile(TIMEZONE_REGEX_PATTERN.encode('utf8')) |
2924 | + |
2925 | +zulu = "Z".encode('latin-1') |
2926 | +minus = "-".encode('latin-1') |
2927 | + |
2928 | +if sys.version_info < (3, 0): |
2929 | + bytes = str |
2930 | + |
2931 | + |
2932 | +class ParseError(Exception): |
2933 | + """Raised when there is a problem parsing a date string""" |
2934 | + |
2935 | +# Yoinked from python docs |
2936 | +ZERO = timedelta(0) |
2937 | +class Utc(tzinfo): |
2938 | + """UTC |
2939 | + |
2940 | + """ |
2941 | + def utcoffset(self, dt): |
2942 | + return ZERO |
2943 | + |
2944 | + def tzname(self, dt): |
2945 | + return "UTC" |
2946 | + |
2947 | + def dst(self, dt): |
2948 | + return ZERO |
2949 | +UTC = Utc() |
2950 | + |
2951 | +class FixedOffset(tzinfo): |
2952 | + """Fixed offset in hours and minutes from UTC |
2953 | + |
2954 | + """ |
2955 | + def __init__(self, offset_hours, offset_minutes, name): |
2956 | + self.__offset = timedelta(hours=offset_hours, minutes=offset_minutes) |
2957 | + self.__name = name |
2958 | + |
2959 | + def utcoffset(self, dt): |
2960 | + return self.__offset |
2961 | + |
2962 | + def tzname(self, dt): |
2963 | + return self.__name |
2964 | + |
2965 | + def dst(self, dt): |
2966 | + return ZERO |
2967 | + |
2968 | + def __repr__(self): |
2969 | + return "<FixedOffset %r>" % self.__name |
2970 | + |
2971 | +def parse_timezone(tzstring, default_timezone=UTC): |
2972 | + """Parses ISO 8601 time zone specs into tzinfo offsets |
2973 | + |
2974 | + """ |
2975 | + if tzstring == zulu: |
2976 | + return default_timezone |
2977 | + # This isn't strictly correct, but it's common to encounter dates without |
2978 | + # timezones so I'll assume the default (which defaults to UTC). |
2979 | + # Addresses issue 4. |
2980 | + if tzstring is None: |
2981 | + return default_timezone |
2982 | + m = TIMEZONE_REGEX.match(tzstring) |
2983 | + prefix, hours, minutes = m.groups() |
2984 | + hours, minutes = int(hours), int(minutes) |
2985 | + if prefix == minus: |
2986 | + hours = -hours |
2987 | + minutes = -minutes |
2988 | + return FixedOffset(hours, minutes, tzstring) |
2989 | + |
2990 | +def parse_date(datestring, default_timezone=UTC): |
2991 | + """Parses ISO 8601 dates into datetime objects |
2992 | + |
2993 | + The timezone is parsed from the date string. However it is quite common to |
2994 | + have dates without a timezone (not strictly correct). In this case the |
2995 | + default timezone specified in default_timezone is used. This is UTC by |
2996 | + default. |
2997 | + """ |
2998 | + if not isinstance(datestring, bytes): |
2999 | + raise ParseError("Expecting bytes %r" % datestring) |
3000 | + m = ISO8601_REGEX.match(datestring) |
3001 | + if not m: |
3002 | + raise ParseError("Unable to parse date string %r" % datestring) |
3003 | + groups = m.groupdict() |
3004 | + tz = parse_timezone(groups["timezone"], default_timezone=default_timezone) |
3005 | + if groups["fraction"] is None: |
3006 | + groups["fraction"] = 0 |
3007 | + else: |
3008 | + groups["fraction"] = int(float("0.%s" % groups["fraction"]) * 1e6) |
3009 | + return datetime(int(groups["year"]), int(groups["month"]), int(groups["day"]), |
3010 | + int(groups["hour"]), int(groups["minute"]), int(groups["second"]), |
3011 | + int(groups["fraction"]), tz) |
3012 | |
3013 | === added file 'test/python/subunit/progress_model.py' |
3014 | --- test/python/subunit/progress_model.py 1970-01-01 00:00:00 +0000 |
3015 | +++ test/python/subunit/progress_model.py 2011-06-10 09:54:56 +0000 |
3016 | @@ -0,0 +1,106 @@ |
3017 | +# |
3018 | +# subunit: extensions to Python unittest to get test results from subprocesses. |
3019 | +# Copyright (C) 2009 Robert Collins <robertc@robertcollins.net> |
3020 | +# |
3021 | +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause |
3022 | +# license at the users choice. A copy of both licenses are available in the |
3023 | +# project source as Apache-2.0 and BSD. You may not use this file except in |
3024 | +# compliance with one of these two licences. |
3025 | +# |
3026 | +# Unless required by applicable law or agreed to in writing, software |
3027 | +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT |
3028 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
3029 | +# license you chose for the specific language governing permissions and |
3030 | +# limitations under that license. |
3031 | +# |
3032 | + |
3033 | +"""Support for dealing with progress state.""" |
3034 | + |
3035 | +class ProgressModel(object): |
3036 | + """A model of progress indicators as subunit defines it. |
3037 | + |
3038 | + Instances of this class represent a single logical operation that is |
3039 | + progressing. The operation may have many steps, and some of those steps may |
3040 | + supply their own progress information. ProgressModel uses a nested concept |
3041 | + where the overall state can be pushed, creating new starting state, and |
3042 | + later pushed to return to the prior state. Many user interfaces will want |
3043 | + to display an overall summary though, and accordingly the pos() and width() |
3044 | + methods return overall summary information rather than information on the |
3045 | + current subtask. |
3046 | + |
3047 | + The default state is 0/0 - indicating that the overall progress is unknown. |
3048 | + Anytime the denominator of pos/width is 0, rendering of a ProgressModel |
3049 | + should should take this into consideration. |
3050 | + |
3051 | + :ivar: _tasks. This private attribute stores the subtasks. Each is a tuple: |
3052 | + pos, width, overall_numerator, overall_denominator. The overall fields |
3053 | + store the calculated overall numerator and denominator for the state |
3054 | + that was pushed. |
3055 | + """ |
3056 | + |
3057 | + def __init__(self): |
3058 | + """Create a ProgressModel. |
3059 | + |
3060 | + The new model has no progress data at all - it will claim a summary |
3061 | + width of zero and position of 0. |
3062 | + """ |
3063 | + self._tasks = [] |
3064 | + self.push() |
3065 | + |
3066 | + def adjust_width(self, offset): |
3067 | + """Adjust the with of the current subtask.""" |
3068 | + self._tasks[-1][1] += offset |
3069 | + |
3070 | + def advance(self): |
3071 | + """Advance the current subtask.""" |
3072 | + self._tasks[-1][0] += 1 |
3073 | + |
3074 | + def pop(self): |
3075 | + """Pop a subtask off the ProgressModel. |
3076 | + |
3077 | + See push for a description of how push and pop work. |
3078 | + """ |
3079 | + self._tasks.pop() |
3080 | + |
3081 | + def pos(self): |
3082 | + """Return how far through the operation has progressed.""" |
3083 | + if not self._tasks: |
3084 | + return 0 |
3085 | + task = self._tasks[-1] |
3086 | + if len(self._tasks) > 1: |
3087 | + # scale up the overall pos by the current task or preserve it if |
3088 | + # no current width is known. |
3089 | + offset = task[2] * (task[1] or 1) |
3090 | + else: |
3091 | + offset = 0 |
3092 | + return offset + task[0] |
3093 | + |
3094 | + def push(self): |
3095 | + """Push a new subtask. |
3096 | + |
3097 | + After pushing a new subtask, the overall progress hasn't changed. Calls |
3098 | + to adjust_width, advance, set_width will only after the progress within |
3099 | + the range that calling 'advance' would have before - the subtask |
3100 | + represents progressing one step in the earlier task. |
3101 | + |
3102 | + Call pop() to restore the progress model to the state before push was |
3103 | + called. |
3104 | + """ |
3105 | + self._tasks.append([0, 0, self.pos(), self.width()]) |
3106 | + |
3107 | + def set_width(self, width): |
3108 | + """Set the width of the current subtask.""" |
3109 | + self._tasks[-1][1] = width |
3110 | + |
3111 | + def width(self): |
3112 | + """Return the total width of the operation.""" |
3113 | + if not self._tasks: |
3114 | + return 0 |
3115 | + task = self._tasks[-1] |
3116 | + if len(self._tasks) > 1: |
3117 | + # scale up the overall width by the current task or preserve it if |
3118 | + # no current width is known. |
3119 | + return task[3] * (task[1] or 1) |
3120 | + else: |
3121 | + return task[1] |
3122 | + |
3123 | |
3124 | === added file 'test/python/subunit/run.py' |
3125 | --- test/python/subunit/run.py 1970-01-01 00:00:00 +0000 |
3126 | +++ test/python/subunit/run.py 2011-06-10 09:54:56 +0000 |
3127 | @@ -0,0 +1,73 @@ |
3128 | +#!/usr/bin/python |
3129 | +# |
3130 | +# Simple subunit testrunner for python |
3131 | +# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007 |
3132 | +# |
3133 | +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause |
3134 | +# license at the users choice. A copy of both licenses are available in the |
3135 | +# project source as Apache-2.0 and BSD. You may not use this file except in |
3136 | +# compliance with one of these two licences. |
3137 | +# |
3138 | +# Unless required by applicable law or agreed to in writing, software |
3139 | +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT |
3140 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
3141 | +# license you chose for the specific language governing permissions and |
3142 | +# limitations under that license. |
3143 | +# |
3144 | + |
3145 | +"""Run a unittest testcase reporting results as Subunit. |
3146 | + |
3147 | + $ python -m subunit.run mylib.tests.test_suite |
3148 | +""" |
3149 | + |
3150 | +import sys |
3151 | + |
3152 | +from subunit import TestProtocolClient, get_default_formatter |
3153 | +from testtools.run import ( |
3154 | + BUFFEROUTPUT, |
3155 | + CATCHBREAK, |
3156 | + FAILFAST, |
3157 | + TestProgram, |
3158 | + USAGE_AS_MAIN, |
3159 | + ) |
3160 | + |
3161 | + |
3162 | +class SubunitTestRunner(object): |
3163 | + def __init__(self, stream=sys.stdout): |
3164 | + self.stream = stream |
3165 | + |
3166 | + def run(self, test): |
3167 | + "Run the given test case or test suite." |
3168 | + result = TestProtocolClient(self.stream) |
3169 | + test(result) |
3170 | + return result |
3171 | + |
3172 | + |
3173 | +class SubunitTestProgram(TestProgram): |
3174 | + |
3175 | + USAGE = USAGE_AS_MAIN |
3176 | + |
3177 | + def usageExit(self, msg=None): |
3178 | + if msg: |
3179 | + print msg |
3180 | + usage = {'progName': self.progName, 'catchbreak': '', 'failfast': '', |
3181 | + 'buffer': ''} |
3182 | + if self.failfast != False: |
3183 | + usage['failfast'] = FAILFAST |
3184 | + if self.catchbreak != False: |
3185 | + usage['catchbreak'] = CATCHBREAK |
3186 | + if self.buffer != False: |
3187 | + usage['buffer'] = BUFFEROUTPUT |
3188 | + usage_text = self.USAGE % usage |
3189 | + usage_lines = usage_text.split('\n') |
3190 | + usage_lines.insert(2, "Run a test suite with a subunit reporter.") |
3191 | + usage_lines.insert(3, "") |
3192 | + print('\n'.join(usage_lines)) |
3193 | + sys.exit(2) |
3194 | + |
3195 | + |
3196 | +if __name__ == '__main__': |
3197 | + stream = get_default_formatter() |
3198 | + runner = SubunitTestRunner(stream) |
3199 | + SubunitTestProgram(module=None, argv=sys.argv, testRunner=runner, |
3200 | + stdout=sys.stdout) |
3201 | |
3202 | === added file 'test/python/subunit/test_results.py' |
3203 | --- test/python/subunit/test_results.py 1970-01-01 00:00:00 +0000 |
3204 | +++ test/python/subunit/test_results.py 2011-06-10 09:54:56 +0000 |
3205 | @@ -0,0 +1,492 @@ |
3206 | +# |
3207 | +# subunit: extensions to Python unittest to get test results from subprocesses. |
3208 | +# Copyright (C) 2009 Robert Collins <robertc@robertcollins.net> |
3209 | +# |
3210 | +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause |
3211 | +# license at the users choice. A copy of both licenses are available in the |
3212 | +# project source as Apache-2.0 and BSD. You may not use this file except in |
3213 | +# compliance with one of these two licences. |
3214 | +# |
3215 | +# Unless required by applicable law or agreed to in writing, software |
3216 | +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT |
3217 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
3218 | +# license you chose for the specific language governing permissions and |
3219 | +# limitations under that license. |
3220 | +# |
3221 | + |
3222 | +"""TestResult helper classes used to by subunit.""" |
3223 | + |
3224 | +import datetime |
3225 | + |
3226 | +import testtools |
3227 | + |
3228 | +from subunit import iso8601 |
3229 | + |
3230 | + |
3231 | +# NOT a TestResult, because we are implementing the interface, not inheriting |
3232 | +# it. |
3233 | +class TestResultDecorator(object): |
3234 | + """General pass-through decorator. |
3235 | + |
3236 | + This provides a base that other TestResults can inherit from to |
3237 | + gain basic forwarding functionality. It also takes care of |
3238 | + handling the case where the target doesn't support newer methods |
3239 | + or features by degrading them. |
3240 | + """ |
3241 | + |
3242 | + def __init__(self, decorated): |
3243 | + """Create a TestResultDecorator forwarding to decorated.""" |
3244 | + # Make every decorator degrade gracefully. |
3245 | + self.decorated = testtools.ExtendedToOriginalDecorator(decorated) |
3246 | + |
3247 | + def startTest(self, test): |
3248 | + return self.decorated.startTest(test) |
3249 | + |
3250 | + def startTestRun(self): |
3251 | + return self.decorated.startTestRun() |
3252 | + |
3253 | + def stopTest(self, test): |
3254 | + return self.decorated.stopTest(test) |
3255 | + |
3256 | + def stopTestRun(self): |
3257 | + return self.decorated.stopTestRun() |
3258 | + |
3259 | + def addError(self, test, err=None, details=None): |
3260 | + return self.decorated.addError(test, err, details=details) |
3261 | + |
3262 | + def addFailure(self, test, err=None, details=None): |
3263 | + return self.decorated.addFailure(test, err, details=details) |
3264 | + |
3265 | + def addSuccess(self, test, details=None): |
3266 | + return self.decorated.addSuccess(test, details=details) |
3267 | + |
3268 | + def addSkip(self, test, reason=None, details=None): |
3269 | + return self.decorated.addSkip(test, reason, details=details) |
3270 | + |
3271 | + def addExpectedFailure(self, test, err=None, details=None): |
3272 | + return self.decorated.addExpectedFailure(test, err, details=details) |
3273 | + |
3274 | + def addUnexpectedSuccess(self, test, details=None): |
3275 | + return self.decorated.addUnexpectedSuccess(test, details=details) |
3276 | + |
3277 | + def progress(self, offset, whence): |
3278 | + return self.decorated.progress(offset, whence) |
3279 | + |
3280 | + def wasSuccessful(self): |
3281 | + return self.decorated.wasSuccessful() |
3282 | + |
3283 | + @property |
3284 | + def shouldStop(self): |
3285 | + return self.decorated.shouldStop |
3286 | + |
3287 | + def stop(self): |
3288 | + return self.decorated.stop() |
3289 | + |
3290 | + @property |
3291 | + def testsRun(self): |
3292 | + return self.decorated.testsRun |
3293 | + |
3294 | + def tags(self, new_tags, gone_tags): |
3295 | + return self.decorated.tags(new_tags, gone_tags) |
3296 | + |
3297 | + def time(self, a_datetime): |
3298 | + return self.decorated.time(a_datetime) |
3299 | + |
3300 | + |
3301 | +class HookedTestResultDecorator(TestResultDecorator): |
3302 | + """A TestResult which calls a hook on every event.""" |
3303 | + |
3304 | + def __init__(self, decorated): |
3305 | + self.super = super(HookedTestResultDecorator, self) |
3306 | + self.super.__init__(decorated) |
3307 | + |
3308 | + def startTest(self, test): |
3309 | + self._before_event() |
3310 | + return self.super.startTest(test) |
3311 | + |
3312 | + def startTestRun(self): |
3313 | + self._before_event() |
3314 | + return self.super.startTestRun() |
3315 | + |
3316 | + def stopTest(self, test): |
3317 | + self._before_event() |
3318 | + return self.super.stopTest(test) |
3319 | + |
3320 | + def stopTestRun(self): |
3321 | + self._before_event() |
3322 | + return self.super.stopTestRun() |
3323 | + |
3324 | + def addError(self, test, err=None, details=None): |
3325 | + self._before_event() |
3326 | + return self.super.addError(test, err, details=details) |
3327 | + |
3328 | + def addFailure(self, test, err=None, details=None): |
3329 | + self._before_event() |
3330 | + return self.super.addFailure(test, err, details=details) |
3331 | + |
3332 | + def addSuccess(self, test, details=None): |
3333 | + self._before_event() |
3334 | + return self.super.addSuccess(test, details=details) |
3335 | + |
3336 | + def addSkip(self, test, reason=None, details=None): |
3337 | + self._before_event() |
3338 | + return self.super.addSkip(test, reason, details=details) |
3339 | + |
3340 | + def addExpectedFailure(self, test, err=None, details=None): |
3341 | + self._before_event() |
3342 | + return self.super.addExpectedFailure(test, err, details=details) |
3343 | + |
3344 | + def addUnexpectedSuccess(self, test, details=None): |
3345 | + self._before_event() |
3346 | + return self.super.addUnexpectedSuccess(test, details=details) |
3347 | + |
3348 | + def progress(self, offset, whence): |
3349 | + self._before_event() |
3350 | + return self.super.progress(offset, whence) |
3351 | + |
3352 | + def wasSuccessful(self): |
3353 | + self._before_event() |
3354 | + return self.super.wasSuccessful() |
3355 | + |
3356 | + @property |
3357 | + def shouldStop(self): |
3358 | + self._before_event() |
3359 | + return self.super.shouldStop |
3360 | + |
3361 | + def stop(self): |
3362 | + self._before_event() |
3363 | + return self.super.stop() |
3364 | + |
3365 | + def time(self, a_datetime): |
3366 | + self._before_event() |
3367 | + return self.super.time(a_datetime) |
3368 | + |
3369 | + |
3370 | +class AutoTimingTestResultDecorator(HookedTestResultDecorator): |
3371 | + """Decorate a TestResult to add time events to a test run. |
3372 | + |
3373 | + By default this will cause a time event before every test event, |
3374 | + but if explicit time data is being provided by the test run, then |
3375 | + this decorator will turn itself off to prevent causing confusion. |
3376 | + """ |
3377 | + |
3378 | + def __init__(self, decorated): |
3379 | + self._time = None |
3380 | + super(AutoTimingTestResultDecorator, self).__init__(decorated) |
3381 | + |
3382 | + def _before_event(self): |
3383 | + time = self._time |
3384 | + if time is not None: |
3385 | + return |
3386 | + time = datetime.datetime.utcnow().replace(tzinfo=iso8601.Utc()) |
3387 | + self.decorated.time(time) |
3388 | + |
3389 | + def progress(self, offset, whence): |
3390 | + return self.decorated.progress(offset, whence) |
3391 | + |
3392 | + @property |
3393 | + def shouldStop(self): |
3394 | + return self.decorated.shouldStop |
3395 | + |
3396 | + def time(self, a_datetime): |
3397 | + """Provide a timestamp for the current test activity. |
3398 | + |
3399 | + :param a_datetime: If None, automatically add timestamps before every |
3400 | + event (this is the default behaviour if time() is not called at |
3401 | + all). If not None, pass the provided time onto the decorated |
3402 | + result object and disable automatic timestamps. |
3403 | + """ |
3404 | + self._time = a_datetime |
3405 | + return self.decorated.time(a_datetime) |
3406 | + |
3407 | + |
3408 | +class TagCollapsingDecorator(TestResultDecorator): |
3409 | + """Collapses many 'tags' calls into one where possible.""" |
3410 | + |
3411 | + def __init__(self, result): |
3412 | + super(TagCollapsingDecorator, self).__init__(result) |
3413 | + # The (new, gone) tags for the current test. |
3414 | + self._current_test_tags = None |
3415 | + |
3416 | + def startTest(self, test): |
3417 | + """Start a test. |
3418 | + |
3419 | + Not directly passed to the client, but used for handling of tags |
3420 | + correctly. |
3421 | + """ |
3422 | + self.decorated.startTest(test) |
3423 | + self._current_test_tags = set(), set() |
3424 | + |
3425 | + def stopTest(self, test): |
3426 | + """Stop a test. |
3427 | + |
3428 | + Not directly passed to the client, but used for handling of tags |
3429 | + correctly. |
3430 | + """ |
3431 | + # Tags to output for this test. |
3432 | + if self._current_test_tags[0] or self._current_test_tags[1]: |
3433 | + self.decorated.tags(*self._current_test_tags) |
3434 | + self.decorated.stopTest(test) |
3435 | + self._current_test_tags = None |
3436 | + |
3437 | + def tags(self, new_tags, gone_tags): |
3438 | + """Handle tag instructions. |
3439 | + |
3440 | + Adds and removes tags as appropriate. If a test is currently running, |
3441 | + tags are not affected for subsequent tests. |
3442 | + |
3443 | + :param new_tags: Tags to add, |
3444 | + :param gone_tags: Tags to remove. |
3445 | + """ |
3446 | + if self._current_test_tags is not None: |
3447 | + # gather the tags until the test stops. |
3448 | + self._current_test_tags[0].update(new_tags) |
3449 | + self._current_test_tags[0].difference_update(gone_tags) |
3450 | + self._current_test_tags[1].update(gone_tags) |
3451 | + self._current_test_tags[1].difference_update(new_tags) |
3452 | + else: |
3453 | + return self.decorated.tags(new_tags, gone_tags) |
3454 | + |
3455 | + |
3456 | +class TimeCollapsingDecorator(HookedTestResultDecorator): |
3457 | + """Only pass on the first and last of a consecutive sequence of times.""" |
3458 | + |
3459 | + def __init__(self, decorated): |
3460 | + super(TimeCollapsingDecorator, self).__init__(decorated) |
3461 | + self._last_received_time = None |
3462 | + self._last_sent_time = None |
3463 | + |
3464 | + def _before_event(self): |
3465 | + if self._last_received_time is None: |
3466 | + return |
3467 | + if self._last_received_time != self._last_sent_time: |
3468 | + self.decorated.time(self._last_received_time) |
3469 | + self._last_sent_time = self._last_received_time |
3470 | + self._last_received_time = None |
3471 | + |
3472 | + def time(self, a_time): |
3473 | + # Don't upcall, because we don't want to call _before_event, it's only |
3474 | + # for non-time events. |
3475 | + if self._last_received_time is None: |
3476 | + self.decorated.time(a_time) |
3477 | + self._last_sent_time = a_time |
3478 | + self._last_received_time = a_time |
3479 | + |
3480 | + |
3481 | +def all_true(bools): |
3482 | + """Return True if all of 'bools' are True. False otherwise.""" |
3483 | + for b in bools: |
3484 | + if not b: |
3485 | + return False |
3486 | + return True |
3487 | + |
3488 | + |
3489 | +class TestResultFilter(TestResultDecorator): |
3490 | + """A pyunit TestResult interface implementation which filters tests. |
3491 | + |
3492 | + Tests that pass the filter are handed on to another TestResult instance |
3493 | + for further processing/reporting. To obtain the filtered results, |
3494 | + the other instance must be interrogated. |
3495 | + |
3496 | + :ivar result: The result that tests are passed to after filtering. |
3497 | + :ivar filter_predicate: The callback run to decide whether to pass |
3498 | + a result. |
3499 | + """ |
3500 | + |
3501 | + def __init__(self, result, filter_error=False, filter_failure=False, |
3502 | + filter_success=True, filter_skip=False, |
3503 | + filter_predicate=None, fixup_expected_failures=None): |
3504 | + """Create a FilterResult object filtering to result. |
3505 | + |
3506 | + :param filter_error: Filter out errors. |
3507 | + :param filter_failure: Filter out failures. |
3508 | + :param filter_success: Filter out successful tests. |
3509 | + :param filter_skip: Filter out skipped tests. |
3510 | + :param filter_predicate: A callable taking (test, outcome, err, |
3511 | + details) and returning True if the result should be passed |
3512 | + through. err and details may be none if no error or extra |
3513 | + metadata is available. outcome is the name of the outcome such |
3514 | + as 'success' or 'failure'. |
3515 | + :param fixup_expected_failures: Set of test ids to consider known |
3516 | + failing. |
3517 | + """ |
3518 | + super(TestResultFilter, self).__init__(result) |
3519 | + self.decorated = TimeCollapsingDecorator( |
3520 | + TagCollapsingDecorator(self.decorated)) |
3521 | + predicates = [] |
3522 | + if filter_error: |
3523 | + predicates.append(lambda t, outcome, e, d: outcome != 'error') |
3524 | + if filter_failure: |
3525 | + predicates.append(lambda t, outcome, e, d: outcome != 'failure') |
3526 | + if filter_success: |
3527 | + predicates.append(lambda t, outcome, e, d: outcome != 'success') |
3528 | + if filter_skip: |
3529 | + predicates.append(lambda t, outcome, e, d: outcome != 'skip') |
3530 | + if filter_predicate is not None: |
3531 | + predicates.append(filter_predicate) |
3532 | + self.filter_predicate = ( |
3533 | + lambda test, outcome, err, details: |
3534 | + all_true(p(test, outcome, err, details) for p in predicates)) |
3535 | + # The current test (for filtering tags) |
3536 | + self._current_test = None |
3537 | + # Has the current test been filtered (for outputting test tags) |
3538 | + self._current_test_filtered = None |
3539 | + # Calls to this result that we don't know whether to forward on yet. |
3540 | + self._buffered_calls = [] |
3541 | + if fixup_expected_failures is None: |
3542 | + self._fixup_expected_failures = frozenset() |
3543 | + else: |
3544 | + self._fixup_expected_failures = fixup_expected_failures |
3545 | + |
3546 | + def addError(self, test, err=None, details=None): |
3547 | + if (self.filter_predicate(test, 'error', err, details)): |
3548 | + if self._failure_expected(test): |
3549 | + self._buffered_calls.append( |
3550 | + ('addExpectedFailure', [test, err], {'details': details})) |
3551 | + else: |
3552 | + self._buffered_calls.append( |
3553 | + ('addError', [test, err], {'details': details})) |
3554 | + else: |
3555 | + self._filtered() |
3556 | + |
3557 | + def addFailure(self, test, err=None, details=None): |
3558 | + if (self.filter_predicate(test, 'failure', err, details)): |
3559 | + if self._failure_expected(test): |
3560 | + self._buffered_calls.append( |
3561 | + ('addExpectedFailure', [test, err], {'details': details})) |
3562 | + else: |
3563 | + self._buffered_calls.append( |
3564 | + ('addFailure', [test, err], {'details': details})) |
3565 | + else: |
3566 | + self._filtered() |
3567 | + |
3568 | + def addSkip(self, test, reason=None, details=None): |
3569 | + if (self.filter_predicate(test, 'skip', reason, details)): |
3570 | + self._buffered_calls.append( |
3571 | + ('addSkip', [test, reason], {'details': details})) |
3572 | + else: |
3573 | + self._filtered() |
3574 | + |
3575 | + def addSuccess(self, test, details=None): |
3576 | + if (self.filter_predicate(test, 'success', None, details)): |
3577 | + if self._failure_expected(test): |
3578 | + self._buffered_calls.append( |
3579 | + ('addUnexpectedSuccess', [test], {'details': details})) |
3580 | + else: |
3581 | + self._buffered_calls.append( |
3582 | + ('addSuccess', [test], {'details': details})) |
3583 | + else: |
3584 | + self._filtered() |
3585 | + |
3586 | + def addExpectedFailure(self, test, err=None, details=None): |
3587 | + if self.filter_predicate(test, 'expectedfailure', err, details): |
3588 | + self._buffered_calls.append( |
3589 | + ('addExpectedFailure', [test, err], {'details': details})) |
3590 | + else: |
3591 | + self._filtered() |
3592 | + |
3593 | + def addUnexpectedSuccess(self, test, details=None): |
3594 | + self._buffered_calls.append( |
3595 | + ('addUnexpectedSuccess', [test], {'details': details})) |
3596 | + |
3597 | + def _filtered(self): |
3598 | + self._current_test_filtered = True |
3599 | + |
3600 | + def _failure_expected(self, test): |
3601 | + return (test.id() in self._fixup_expected_failures) |
3602 | + |
3603 | + def startTest(self, test): |
3604 | + """Start a test. |
3605 | + |
3606 | + Not directly passed to the client, but used for handling of tags |
3607 | + correctly. |
3608 | + """ |
3609 | + self._current_test = test |
3610 | + self._current_test_filtered = False |
3611 | + self._buffered_calls.append(('startTest', [test], {})) |
3612 | + |
3613 | + def stopTest(self, test): |
3614 | + """Stop a test. |
3615 | + |
3616 | + Not directly passed to the client, but used for handling of tags |
3617 | + correctly. |
3618 | + """ |
3619 | + if not self._current_test_filtered: |
3620 | + # Tags to output for this test. |
3621 | + for method, args, kwargs in self._buffered_calls: |
3622 | + getattr(self.decorated, method)(*args, **kwargs) |
3623 | + self.decorated.stopTest(test) |
3624 | + self._current_test = None |
3625 | + self._current_test_filtered = None |
3626 | + self._buffered_calls = [] |
3627 | + |
3628 | + def time(self, a_time): |
3629 | + if self._current_test is not None: |
3630 | + self._buffered_calls.append(('time', [a_time], {})) |
3631 | + else: |
3632 | + return self.decorated.time(a_time) |
3633 | + |
3634 | + def id_to_orig_id(self, id): |
3635 | + if id.startswith("subunit.RemotedTestCase."): |
3636 | + return id[len("subunit.RemotedTestCase."):] |
3637 | + return id |
3638 | + |
3639 | + |
3640 | +class TestIdPrintingResult(testtools.TestResult): |
3641 | + |
3642 | + def __init__(self, stream, show_times=False): |
3643 | + """Create a FilterResult object outputting to stream.""" |
3644 | + super(TestIdPrintingResult, self).__init__() |
3645 | + self._stream = stream |
3646 | + self.failed_tests = 0 |
3647 | + self.__time = None |
3648 | + self.show_times = show_times |
3649 | + self._test = None |
3650 | + self._test_duration = 0 |
3651 | + |
3652 | + def addError(self, test, err): |
3653 | + self.failed_tests += 1 |
3654 | + self._test = test |
3655 | + |
3656 | + def addFailure(self, test, err): |
3657 | + self.failed_tests += 1 |
3658 | + self._test = test |
3659 | + |
3660 | + def addSuccess(self, test): |
3661 | + self._test = test |
3662 | + |
3663 | + def addSkip(self, test, reason=None, details=None): |
3664 | + self._test = test |
3665 | + |
3666 | + def addUnexpectedSuccess(self, test, details=None): |
3667 | + self.failed_tests += 1 |
3668 | + self._test = test |
3669 | + |
3670 | + def addExpectedFailure(self, test, err=None, details=None): |
3671 | + self._test = test |
3672 | + |
3673 | + def reportTest(self, test, duration): |
3674 | + if self.show_times: |
3675 | + seconds = duration.seconds |
3676 | + seconds += duration.days * 3600 * 24 |
3677 | + seconds += duration.microseconds / 1000000.0 |
3678 | + self._stream.write(test.id() + ' %0.3f\n' % seconds) |
3679 | + else: |
3680 | + self._stream.write(test.id() + '\n') |
3681 | + |
3682 | + def startTest(self, test): |
3683 | + self._start_time = self._time() |
3684 | + |
3685 | + def stopTest(self, test): |
3686 | + test_duration = self._time() - self._start_time |
3687 | + self.reportTest(self._test, test_duration) |
3688 | + |
3689 | + def time(self, time): |
3690 | + self.__time = time |
3691 | + |
3692 | + def _time(self): |
3693 | + return self.__time |
3694 | + |
3695 | + def wasSuccessful(self): |
3696 | + "Tells whether or not this result was a success" |
3697 | + return self.failed_tests == 0 |
3698 | |
3699 | === added directory 'test/python/subunit/tests' |
3700 | === added file 'test/python/subunit/tests/TestUtil.py' |
3701 | --- test/python/subunit/tests/TestUtil.py 1970-01-01 00:00:00 +0000 |
3702 | +++ test/python/subunit/tests/TestUtil.py 2011-06-10 09:54:56 +0000 |
3703 | @@ -0,0 +1,80 @@ |
3704 | +# Copyright (c) 2004 Canonical Limited |
3705 | +# Author: Robert Collins <robert.collins@canonical.com> |
3706 | +# |
3707 | +# This program is free software; you can redistribute it and/or modify |
3708 | +# it under the terms of the GNU General Public License as published by |
3709 | +# the Free Software Foundation; either version 2 of the License, or |
3710 | +# (at your option) any later version. |
3711 | +# |
3712 | +# This program is distributed in the hope that it will be useful, |
3713 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
3714 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
3715 | +# GNU General Public License for more details. |
3716 | +# |
3717 | +# You should have received a copy of the GNU General Public License |
3718 | +# along with this program; if not, write to the Free Software |
3719 | +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
3720 | +# |
3721 | + |
3722 | +import sys |
3723 | +import logging |
3724 | +import unittest |
3725 | + |
3726 | + |
3727 | +class LogCollector(logging.Handler): |
3728 | + def __init__(self): |
3729 | + logging.Handler.__init__(self) |
3730 | + self.records=[] |
3731 | + def emit(self, record): |
3732 | + self.records.append(record.getMessage()) |
3733 | + |
3734 | + |
3735 | +def makeCollectingLogger(): |
3736 | + """I make a logger instance that collects its logs for programmatic analysis |
3737 | + -> (logger, collector)""" |
3738 | + logger=logging.Logger("collector") |
3739 | + handler=LogCollector() |
3740 | + handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s")) |
3741 | + logger.addHandler(handler) |
3742 | + return logger, handler |
3743 | + |
3744 | + |
3745 | +def visitTests(suite, visitor): |
3746 | + """A foreign method for visiting the tests in a test suite.""" |
3747 | + for test in suite._tests: |
3748 | + #Abusing types to avoid monkey patching unittest.TestCase. |
3749 | + # Maybe that would be better? |
3750 | + try: |
3751 | + test.visit(visitor) |
3752 | + except AttributeError: |
3753 | + if isinstance(test, unittest.TestCase): |
3754 | + visitor.visitCase(test) |
3755 | + elif isinstance(test, unittest.TestSuite): |
3756 | + visitor.visitSuite(test) |
3757 | + visitTests(test, visitor) |
3758 | + else: |
3759 | + print ("unvisitable non-unittest.TestCase element %r (%r)" % (test, test.__class__)) |
3760 | + |
3761 | + |
3762 | +class TestSuite(unittest.TestSuite): |
3763 | + """I am an extended TestSuite with a visitor interface. |
3764 | + This is primarily to allow filtering of tests - and suites or |
3765 | + more in the future. An iterator of just tests wouldn't scale...""" |
3766 | + |
3767 | + def visit(self, visitor): |
3768 | + """visit the composite. Visiting is depth-first. |
3769 | + current callbacks are visitSuite and visitCase.""" |
3770 | + visitor.visitSuite(self) |
3771 | + visitTests(self, visitor) |
3772 | + |
3773 | + |
3774 | +class TestLoader(unittest.TestLoader): |
3775 | + """Custome TestLoader to set the right TestSuite class.""" |
3776 | + suiteClass = TestSuite |
3777 | + |
3778 | +class TestVisitor(object): |
3779 | + """A visitor for Tests""" |
3780 | + def visitSuite(self, aTestSuite): |
3781 | + pass |
3782 | + def visitCase(self, aTestCase): |
3783 | + pass |
3784 | |
3785 | === added file 'test/python/subunit/tests/__init__.py' |
3786 | --- test/python/subunit/tests/__init__.py 1970-01-01 00:00:00 +0000 |
3787 | +++ test/python/subunit/tests/__init__.py 2011-06-10 09:54:56 +0000 |
3788 | @@ -0,0 +1,41 @@ |
3789 | +# |
3790 | +# subunit: extensions to python unittest to get test results from subprocesses. |
3791 | +# Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> |
3792 | +# |
3793 | +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause |
3794 | +# license at the users choice. A copy of both licenses are available in the |
3795 | +# project source as Apache-2.0 and BSD. You may not use this file except in |
3796 | +# compliance with one of these two licences. |
3797 | +# |
3798 | +# Unless required by applicable law or agreed to in writing, software |
3799 | +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT |
3800 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
3801 | +# license you chose for the specific language governing permissions and |
3802 | +# limitations under that license. |
3803 | +# |
3804 | + |
3805 | +from subunit.tests import ( |
3806 | + TestUtil, |
3807 | + test_chunked, |
3808 | + test_details, |
3809 | + test_progress_model, |
3810 | + test_subunit_filter, |
3811 | + test_subunit_stats, |
3812 | + test_subunit_tags, |
3813 | + test_tap2subunit, |
3814 | + test_test_protocol, |
3815 | + test_test_results, |
3816 | + ) |
3817 | + |
3818 | +def test_suite(): |
3819 | + result = TestUtil.TestSuite() |
3820 | + result.addTest(test_chunked.test_suite()) |
3821 | + result.addTest(test_details.test_suite()) |
3822 | + result.addTest(test_progress_model.test_suite()) |
3823 | + result.addTest(test_test_results.test_suite()) |
3824 | + result.addTest(test_test_protocol.test_suite()) |
3825 | + result.addTest(test_tap2subunit.test_suite()) |
3826 | + result.addTest(test_subunit_filter.test_suite()) |
3827 | + result.addTest(test_subunit_tags.test_suite()) |
3828 | + result.addTest(test_subunit_stats.test_suite()) |
3829 | + return result |
3830 | |
3831 | === added file 'test/python/subunit/tests/sample-script.py' |
3832 | --- test/python/subunit/tests/sample-script.py 1970-01-01 00:00:00 +0000 |
3833 | +++ test/python/subunit/tests/sample-script.py 2011-06-10 09:54:56 +0000 |
3834 | @@ -0,0 +1,21 @@ |
3835 | +#!/usr/bin/env python |
3836 | +import sys |
3837 | +if sys.platform == "win32": |
3838 | + import msvcrt, os |
3839 | + msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) |
3840 | +if len(sys.argv) == 2: |
3841 | + # subunit.tests.test_test_protocol.TestExecTestCase.test_sample_method_args |
3842 | + # uses this code path to be sure that the arguments were passed to |
3843 | + # sample-script.py |
3844 | + print "test fail" |
3845 | + print "error fail" |
3846 | + sys.exit(0) |
3847 | +print "test old mcdonald" |
3848 | +print "success old mcdonald" |
3849 | +print "test bing crosby" |
3850 | +print "failure bing crosby [" |
3851 | +print "foo.c:53:ERROR invalid state" |
3852 | +print "]" |
3853 | +print "test an error" |
3854 | +print "error an error" |
3855 | +sys.exit(0) |
3856 | |
3857 | === added file 'test/python/subunit/tests/sample-two-script.py' |
3858 | --- test/python/subunit/tests/sample-two-script.py 1970-01-01 00:00:00 +0000 |
3859 | +++ test/python/subunit/tests/sample-two-script.py 2011-06-10 09:54:56 +0000 |
3860 | @@ -0,0 +1,7 @@ |
3861 | +#!/usr/bin/env python |
3862 | +import sys |
3863 | +print "test old mcdonald" |
3864 | +print "success old mcdonald" |
3865 | +print "test bing crosby" |
3866 | +print "success bing crosby" |
3867 | +sys.exit(0) |
3868 | |
3869 | === added file 'test/python/subunit/tests/test_chunked.py' |
3870 | --- test/python/subunit/tests/test_chunked.py 1970-01-01 00:00:00 +0000 |
3871 | +++ test/python/subunit/tests/test_chunked.py 2011-06-10 09:54:56 +0000 |
3872 | @@ -0,0 +1,152 @@ |
3873 | +# |
3874 | +# subunit: extensions to python unittest to get test results from subprocesses. |
3875 | +# Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> |
3876 | +# Copyright (C) 2011 Martin Pool <mbp@sourcefrog.net> |
3877 | +# |
3878 | +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause |
3879 | +# license at the users choice. A copy of both licenses are available in the |
3880 | +# project source as Apache-2.0 and BSD. You may not use this file except in |
3881 | +# compliance with one of these two licences. |
3882 | +# |
3883 | +# Unless required by applicable law or agreed to in writing, software |
3884 | +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT |
3885 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
3886 | +# license you chose for the specific language governing permissions and |
3887 | +# limitations under that license. |
3888 | +# |
3889 | + |
3890 | +import unittest |
3891 | + |
3892 | +from testtools.compat import _b, BytesIO |
3893 | + |
3894 | +import subunit.chunked |
3895 | + |
3896 | + |
3897 | +def test_suite(): |
3898 | + loader = subunit.tests.TestUtil.TestLoader() |
3899 | + result = loader.loadTestsFromName(__name__) |
3900 | + return result |
3901 | + |
3902 | + |
3903 | +class TestDecode(unittest.TestCase): |
3904 | + |
3905 | + def setUp(self): |
3906 | + unittest.TestCase.setUp(self) |
3907 | + self.output = BytesIO() |
3908 | + self.decoder = subunit.chunked.Decoder(self.output) |
3909 | + |
3910 | + def test_close_read_length_short_errors(self): |
3911 | + self.assertRaises(ValueError, self.decoder.close) |
3912 | + |
3913 | + def test_close_body_short_errors(self): |
3914 | + self.assertEqual(None, self.decoder.write(_b('2\r\na'))) |
3915 | + self.assertRaises(ValueError, self.decoder.close) |
3916 | + |
3917 | + def test_close_body_buffered_data_errors(self): |
3918 | + self.assertEqual(None, self.decoder.write(_b('2\r'))) |
3919 | + self.assertRaises(ValueError, self.decoder.close) |
3920 | + |
3921 | + def test_close_after_finished_stream_safe(self): |
3922 | + self.assertEqual(None, self.decoder.write(_b('2\r\nab'))) |
3923 | + self.assertEqual(_b(''), self.decoder.write(_b('0\r\n'))) |
3924 | + self.decoder.close() |
3925 | + |
3926 | + def test_decode_nothing(self): |
3927 | + self.assertEqual(_b(''), self.decoder.write(_b('0\r\n'))) |
3928 | + self.assertEqual(_b(''), self.output.getvalue()) |
3929 | + |
3930 | + def test_decode_serialised_form(self): |
3931 | + self.assertEqual(None, self.decoder.write(_b("F\r\n"))) |
3932 | + self.assertEqual(None, self.decoder.write(_b("serialised\n"))) |
3933 | + self.assertEqual(_b(''), self.decoder.write(_b("form0\r\n"))) |
3934 | + |
3935 | + def test_decode_short(self): |
3936 | + self.assertEqual(_b(''), self.decoder.write(_b('3\r\nabc0\r\n'))) |
3937 | + self.assertEqual(_b('abc'), self.output.getvalue()) |
3938 | + |
3939 | + def test_decode_combines_short(self): |
3940 | + self.assertEqual(_b(''), self.decoder.write(_b('6\r\nabcdef0\r\n'))) |
3941 | + self.assertEqual(_b('abcdef'), self.output.getvalue()) |
3942 | + |
3943 | + def test_decode_excess_bytes_from_write(self): |
3944 | + self.assertEqual(_b('1234'), self.decoder.write(_b('3\r\nabc0\r\n1234'))) |
3945 | + self.assertEqual(_b('abc'), self.output.getvalue()) |
3946 | + |
3947 | + def test_decode_write_after_finished_errors(self): |
3948 | + self.assertEqual(_b('1234'), self.decoder.write(_b('3\r\nabc0\r\n1234'))) |
3949 | + self.assertRaises(ValueError, self.decoder.write, _b('')) |
3950 | + |
3951 | + def test_decode_hex(self): |
3952 | + self.assertEqual(_b(''), self.decoder.write(_b('A\r\n12345678900\r\n'))) |
3953 | + self.assertEqual(_b('1234567890'), self.output.getvalue()) |
3954 | + |
3955 | + def test_decode_long_ranges(self): |
3956 | + self.assertEqual(None, self.decoder.write(_b('10000\r\n'))) |
3957 | + self.assertEqual(None, self.decoder.write(_b('1' * 65536))) |
3958 | + self.assertEqual(None, self.decoder.write(_b('10000\r\n'))) |
3959 | + self.assertEqual(None, self.decoder.write(_b('2' * 65536))) |
3960 | + self.assertEqual(_b(''), self.decoder.write(_b('0\r\n'))) |
3961 | + self.assertEqual(_b('1' * 65536 + '2' * 65536), self.output.getvalue()) |
3962 | + |
3963 | + def test_decode_newline_nonstrict(self): |
3964 | + """Tolerate chunk markers with no CR character.""" |
3965 | + # From <http://pad.lv/505078> |
3966 | + self.decoder = subunit.chunked.Decoder(self.output, strict=False) |
3967 | + self.assertEqual(None, self.decoder.write(_b('a\n'))) |
3968 | + self.assertEqual(None, self.decoder.write(_b('abcdeabcde'))) |
3969 | + self.assertEqual(_b(''), self.decoder.write(_b('0\n'))) |
3970 | + self.assertEqual(_b('abcdeabcde'), self.output.getvalue()) |
3971 | + |
3972 | + def test_decode_strict_newline_only(self): |
3973 | + """Reject chunk markers with no CR character in strict mode.""" |
3974 | + # From <http://pad.lv/505078> |
3975 | + self.assertRaises(ValueError, |
3976 | + self.decoder.write, _b('a\n')) |
3977 | + |
3978 | + def test_decode_strict_multiple_crs(self): |
3979 | + self.assertRaises(ValueError, |
3980 | + self.decoder.write, _b('a\r\r\n')) |
3981 | + |
3982 | + def test_decode_short_header(self): |
3983 | + self.assertRaises(ValueError, |
3984 | + self.decoder.write, _b('\n')) |
3985 | + |
3986 | + |
3987 | +class TestEncode(unittest.TestCase): |
3988 | + |
3989 | + def setUp(self): |
3990 | + unittest.TestCase.setUp(self) |
3991 | + self.output = BytesIO() |
3992 | + self.encoder = subunit.chunked.Encoder(self.output) |
3993 | + |
3994 | + def test_encode_nothing(self): |
3995 | + self.encoder.close() |
3996 | + self.assertEqual(_b('0\r\n'), self.output.getvalue()) |
3997 | + |
3998 | + def test_encode_empty(self): |
3999 | + self.encoder.write(_b('')) |
4000 | + self.encoder.close() |
4001 | + self.assertEqual(_b('0\r\n'), self.output.getvalue()) |
4002 | + |
4003 | + def test_encode_short(self): |
4004 | + self.encoder.write(_b('abc')) |
4005 | + self.encoder.close() |
4006 | + self.assertEqual(_b('3\r\nabc0\r\n'), self.output.getvalue()) |
4007 | + |
4008 | + def test_encode_combines_short(self): |
4009 | + self.encoder.write(_b('abc')) |
4010 | + self.encoder.write(_b('def')) |
4011 | + self.encoder.close() |
4012 | + self.assertEqual(_b('6\r\nabcdef0\r\n'), self.output.getvalue()) |
4013 | + |
4014 | + def test_encode_over_9_is_in_hex(self): |
4015 | + self.encoder.write(_b('1234567890')) |
4016 | + self.encoder.close() |
4017 | + self.assertEqual(_b('A\r\n12345678900\r\n'), self.output.getvalue()) |
4018 | + |
4019 | + def test_encode_long_ranges_not_combined(self): |
4020 | + self.encoder.write(_b('1' * 65536)) |
4021 | + self.encoder.write(_b('2' * 65536)) |
4022 | + self.encoder.close() |
4023 | + self.assertEqual(_b('10000\r\n' + '1' * 65536 + '10000\r\n' + |
4024 | + '2' * 65536 + '0\r\n'), self.output.getvalue()) |
4025 | |
4026 | === added file 'test/python/subunit/tests/test_details.py' |
4027 | --- test/python/subunit/tests/test_details.py 1970-01-01 00:00:00 +0000 |
4028 | +++ test/python/subunit/tests/test_details.py 2011-06-10 09:54:56 +0000 |
4029 | @@ -0,0 +1,112 @@ |
4030 | +# |
4031 | +# subunit: extensions to python unittest to get test results from subprocesses. |
4032 | +# Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> |
4033 | +# |
4034 | +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause |
4035 | +# license at the users choice. A copy of both licenses are available in the |
4036 | +# project source as Apache-2.0 and BSD. You may not use this file except in |
4037 | +# compliance with one of these two licences. |
4038 | +# |
4039 | +# Unless required by applicable law or agreed to in writing, software |
4040 | +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT |
4041 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
4042 | +# license you chose for the specific language governing permissions and |
4043 | +# limitations under that license. |
4044 | +# |
4045 | + |
4046 | +import unittest |
4047 | + |
4048 | +from testtools.compat import _b, StringIO |
4049 | + |
4050 | +import subunit.tests |
4051 | +from subunit import content, content_type, details |
4052 | + |
4053 | + |
4054 | +def test_suite(): |
4055 | + loader = subunit.tests.TestUtil.TestLoader() |
4056 | + result = loader.loadTestsFromName(__name__) |
4057 | + return result |
4058 | + |
4059 | + |
4060 | +class TestSimpleDetails(unittest.TestCase): |
4061 | + |
4062 | + def test_lineReceived(self): |
4063 | + parser = details.SimpleDetailsParser(None) |
4064 | + parser.lineReceived(_b("foo\n")) |
4065 | + parser.lineReceived(_b("bar\n")) |
4066 | + self.assertEqual(_b("foo\nbar\n"), parser._message) |
4067 | + |
4068 | + def test_lineReceived_escaped_bracket(self): |
4069 | + parser = details.SimpleDetailsParser(None) |
4070 | + parser.lineReceived(_b("foo\n")) |
4071 | + parser.lineReceived(_b(" ]are\n")) |
4072 | + parser.lineReceived(_b("bar\n")) |
4073 | + self.assertEqual(_b("foo\n]are\nbar\n"), parser._message) |
4074 | + |
4075 | + def test_get_message(self): |
4076 | + parser = details.SimpleDetailsParser(None) |
4077 | + self.assertEqual(_b(""), parser.get_message()) |
4078 | + |
4079 | + def test_get_details(self): |
4080 | + parser = details.SimpleDetailsParser(None) |
4081 | + traceback = "" |
4082 | + expected = {} |
4083 | + expected['traceback'] = content.Content( |
4084 | + content_type.ContentType("text", "x-traceback", |
4085 | + {'charset': 'utf8'}), |
4086 | + lambda:[_b("")]) |
4087 | + found = parser.get_details() |
4088 | + self.assertEqual(expected.keys(), found.keys()) |
4089 | + self.assertEqual(expected['traceback'].content_type, |
4090 | + found['traceback'].content_type) |
4091 | + self.assertEqual(_b('').join(expected['traceback'].iter_bytes()), |
4092 | + _b('').join(found['traceback'].iter_bytes())) |
4093 | + |
4094 | + def test_get_details_skip(self): |
4095 | + parser = details.SimpleDetailsParser(None) |
4096 | + traceback = "" |
4097 | + expected = {} |
4098 | + expected['reason'] = content.Content( |
4099 | + content_type.ContentType("text", "plain"), |
4100 | + lambda:[_b("")]) |
4101 | + found = parser.get_details("skip") |
4102 | + self.assertEqual(expected, found) |
4103 | + |
4104 | + def test_get_details_success(self): |
4105 | + parser = details.SimpleDetailsParser(None) |
4106 | + traceback = "" |
4107 | + expected = {} |
4108 | + expected['message'] = content.Content( |
4109 | + content_type.ContentType("text", "plain"), |
4110 | + lambda:[_b("")]) |
4111 | + found = parser.get_details("success") |
4112 | + self.assertEqual(expected, found) |
4113 | + |
4114 | + |
4115 | +class TestMultipartDetails(unittest.TestCase): |
4116 | + |
4117 | + def test_get_message_is_None(self): |
4118 | + parser = details.MultipartDetailsParser(None) |
4119 | + self.assertEqual(None, parser.get_message()) |
4120 | + |
4121 | + def test_get_details(self): |
4122 | + parser = details.MultipartDetailsParser(None) |
4123 | + self.assertEqual({}, parser.get_details()) |
4124 | + |
4125 | + def test_parts(self): |
4126 | + parser = details.MultipartDetailsParser(None) |
4127 | + parser.lineReceived(_b("Content-Type: text/plain\n")) |
4128 | + parser.lineReceived(_b("something\n")) |
4129 | + parser.lineReceived(_b("F\r\n")) |
4130 | + parser.lineReceived(_b("serialised\n")) |
4131 | + parser.lineReceived(_b("form0\r\n")) |
4132 | + expected = {} |
4133 | + expected['something'] = content.Content( |
4134 | + content_type.ContentType("text", "plain"), |
4135 | + lambda:[_b("serialised\nform")]) |
4136 | + found = parser.get_details() |
4137 | + self.assertEqual(expected.keys(), found.keys()) |
4138 | + self.assertEqual(expected['something'].content_type, |
4139 | + found['something'].content_type) |
4140 | + self.assertEqual(_b('').join(expected['something'].iter_bytes()), |
4141 | + _b('').join(found['something'].iter_bytes())) |
4142 | |
4143 | === added file 'test/python/subunit/tests/test_progress_model.py' |
4144 | --- test/python/subunit/tests/test_progress_model.py 1970-01-01 00:00:00 +0000 |
4145 | +++ test/python/subunit/tests/test_progress_model.py 2011-06-10 09:54:56 +0000 |
4146 | @@ -0,0 +1,118 @@ |
4147 | +# |
4148 | +# subunit: extensions to Python unittest to get test results from subprocesses. |
4149 | +# Copyright (C) 2009 Robert Collins <robertc@robertcollins.net> |
4150 | +# |
4151 | +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause |
4152 | +# license at the users choice. A copy of both licenses are available in the |
4153 | +# project source as Apache-2.0 and BSD. You may not use this file except in |
4154 | +# compliance with one of these two licences. |
4155 | +# |
4156 | +# Unless required by applicable law or agreed to in writing, software |
4157 | +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT |
4158 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
4159 | +# license you chose for the specific language governing permissions and |
4160 | +# limitations under that license. |
4161 | +# |
4162 | + |
4163 | +import unittest |
4164 | + |
4165 | +import subunit |
4166 | +from subunit.progress_model import ProgressModel |
4167 | + |
4168 | + |
4169 | +class TestProgressModel(unittest.TestCase): |
4170 | + |
4171 | + def assertProgressSummary(self, pos, total, progress): |
4172 | + """Assert that a progress model has reached a particular point.""" |
4173 | + self.assertEqual(pos, progress.pos()) |
4174 | + self.assertEqual(total, progress.width()) |
4175 | + |
4176 | + def test_new_progress_0_0(self): |
4177 | + progress = ProgressModel() |
4178 | + self.assertProgressSummary(0, 0, progress) |
4179 | + |
4180 | + def test_advance_0_0(self): |
4181 | + progress = ProgressModel() |
4182 | + progress.advance() |
4183 | + self.assertProgressSummary(1, 0, progress) |
4184 | + |
4185 | + def test_advance_1_0(self): |
4186 | + progress = ProgressModel() |
4187 | + progress.advance() |
4188 | + self.assertProgressSummary(1, 0, progress) |
4189 | + |
4190 | + def test_set_width_absolute(self): |
4191 | + progress = ProgressModel() |
4192 | + progress.set_width(10) |
4193 | + self.assertProgressSummary(0, 10, progress) |
4194 | + |
4195 | + def test_set_width_absolute_preserves_pos(self): |
4196 | + progress = ProgressModel() |
4197 | + progress.advance() |
4198 | + progress.set_width(2) |
4199 | + self.assertProgressSummary(1, 2, progress) |
4200 | + |
4201 | + def test_adjust_width(self): |
4202 | + progress = ProgressModel() |
4203 | + progress.adjust_width(10) |
4204 | + self.assertProgressSummary(0, 10, progress) |
4205 | + progress.adjust_width(-10) |
4206 | + self.assertProgressSummary(0, 0, progress) |
4207 | + |
4208 | + def test_adjust_width_preserves_pos(self): |
4209 | + progress = ProgressModel() |
4210 | + progress.advance() |
4211 | + progress.adjust_width(10) |
4212 | + self.assertProgressSummary(1, 10, progress) |
4213 | + progress.adjust_width(-10) |
4214 | + self.assertProgressSummary(1, 0, progress) |
4215 | + |
4216 | + def test_push_preserves_progress(self): |
4217 | + progress = ProgressModel() |
4218 | + progress.adjust_width(3) |
4219 | + progress.advance() |
4220 | + progress.push() |
4221 | + self.assertProgressSummary(1, 3, progress) |
4222 | + |
4223 | + def test_advance_advances_substack(self): |
4224 | + progress = ProgressModel() |
4225 | + progress.adjust_width(3) |
4226 | + progress.advance() |
4227 | + progress.push() |
4228 | + progress.adjust_width(1) |
4229 | + progress.advance() |
4230 | + self.assertProgressSummary(2, 3, progress) |
4231 | + |
4232 | + def test_adjust_width_adjusts_substack(self): |
4233 | + progress = ProgressModel() |
4234 | + progress.adjust_width(3) |
4235 | + progress.advance() |
4236 | + progress.push() |
4237 | + progress.adjust_width(2) |
4238 | + progress.advance() |
4239 | + self.assertProgressSummary(3, 6, progress) |
4240 | + |
4241 | + def test_set_width_adjusts_substack(self): |
4242 | + progress = ProgressModel() |
4243 | + progress.adjust_width(3) |
4244 | + progress.advance() |
4245 | + progress.push() |
4246 | + progress.set_width(2) |
4247 | + progress.advance() |
4248 | + self.assertProgressSummary(3, 6, progress) |
4249 | + |
4250 | + def test_pop_restores_progress(self): |
4251 | + progress = ProgressModel() |
4252 | + progress.adjust_width(3) |
4253 | + progress.advance() |
4254 | + progress.push() |
4255 | + progress.adjust_width(1) |
4256 | + progress.advance() |
4257 | + progress.pop() |
4258 | + self.assertProgressSummary(1, 3, progress) |
4259 | + |
4260 | + |
4261 | +def test_suite(): |
4262 | + loader = subunit.tests.TestUtil.TestLoader() |
4263 | + result = loader.loadTestsFromName(__name__) |
4264 | + return result |
4265 | |
4266 | === added file 'test/python/subunit/tests/test_subunit_filter.py' |
4267 | --- test/python/subunit/tests/test_subunit_filter.py 1970-01-01 00:00:00 +0000 |
4268 | +++ test/python/subunit/tests/test_subunit_filter.py 2011-06-10 09:54:56 +0000 |
4269 | @@ -0,0 +1,208 @@ |
4270 | +# |
4271 | +# subunit: extensions to python unittest to get test results from subprocesses. |
4272 | +# Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> |
4273 | +# |
4274 | +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause |
4275 | +# license at the users choice. A copy of both licenses are available in the |
4276 | +# project source as Apache-2.0 and BSD. You may not use this file except in |
4277 | +# compliance with one of these two licences. |
4278 | +# |
4279 | +# Unless required by applicable law or agreed to in writing, software |
4280 | +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT |
4281 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
4282 | +# license you chose for the specific language governing permissions and |
4283 | +# limitations under that license. |
4284 | +# |
4285 | + |
4286 | +"""Tests for subunit.TestResultFilter.""" |
4287 | + |
4288 | +from datetime import datetime |
4289 | +from subunit import iso8601 |
4290 | +import unittest |
4291 | + |
4292 | +from testtools import TestCase |
4293 | +from testtools.compat import _b, BytesIO, StringIO |
4294 | +from testtools.testresult.doubles import ExtendedTestResult |
4295 | + |
4296 | +import subunit |
4297 | +from subunit.test_results import TestResultFilter |
4298 | + |
4299 | + |
4300 | +class TestTestResultFilter(TestCase): |
4301 | + """Test for TestResultFilter, a TestResult object which filters tests.""" |
4302 | + |
4303 | + # While TestResultFilter works on python objects, using a subunit stream |
4304 | + # is an easy pithy way of getting a series of test objects to call into |
4305 | + # the TestResult, and as TestResultFilter is intended for use with subunit |
4306 | + # also has the benefit of detecting any interface skew issues. |
4307 | + example_subunit_stream = _b("""\ |
4308 | +tags: global |
4309 | +test passed |
4310 | +success passed |
4311 | +test failed |
4312 | +tags: local |
4313 | +failure failed |
4314 | +test error |
4315 | +error error [ |
4316 | +error details |
4317 | +] |
4318 | +test skipped |
4319 | +skip skipped |
4320 | +test todo |
4321 | +xfail todo |
4322 | +""") |
4323 | + |
4324 | + def run_tests(self, result_filter, input_stream=None): |
4325 | + """Run tests through the given filter. |
4326 | + |
4327 | + :param result_filter: A filtering TestResult object. |
4328 | + :param input_stream: Bytes of subunit stream data. If not provided, |
4329 | + uses TestTestResultFilter.example_subunit_stream. |
4330 | + """ |
4331 | + if input_stream is None: |
4332 | + input_stream = self.example_subunit_stream |
4333 | + test = subunit.ProtocolTestCase(BytesIO(input_stream)) |
4334 | + test.run(result_filter) |
4335 | + |
4336 | + def test_default(self): |
4337 | + """The default is to exclude success and include everything else.""" |
4338 | + filtered_result = unittest.TestResult() |
4339 | + result_filter = TestResultFilter(filtered_result) |
4340 | + self.run_tests(result_filter) |
4341 | + # skips are seen as success by default python TestResult. |
4342 | + self.assertEqual(['error'], |
4343 | + [error[0].id() for error in filtered_result.errors]) |
4344 | + self.assertEqual(['failed'], |
4345 | + [failure[0].id() for failure in |
4346 | + filtered_result.failures]) |
4347 | + self.assertEqual(4, filtered_result.testsRun) |
4348 | + |
4349 | + def test_exclude_errors(self): |
4350 | + filtered_result = unittest.TestResult() |
4351 | + result_filter = TestResultFilter(filtered_result, filter_error=True) |
4352 | + self.run_tests(result_filter) |
4353 | + # skips are seen as errors by default python TestResult. |
4354 | + self.assertEqual([], filtered_result.errors) |
4355 | + self.assertEqual(['failed'], |
4356 | + [failure[0].id() for failure in |
4357 | + filtered_result.failures]) |
4358 | + self.assertEqual(3, filtered_result.testsRun) |
4359 | + |
4360 | + def test_fixup_expected_failures(self): |
4361 | + filtered_result = unittest.TestResult() |
4362 | + result_filter = TestResultFilter(filtered_result, |
4363 | + fixup_expected_failures=set(["failed"])) |
4364 | + self.run_tests(result_filter) |
4365 | + self.assertEqual(['failed', 'todo'], |
4366 | + [failure[0].id() for failure in filtered_result.expectedFailures]) |
4367 | + self.assertEqual([], filtered_result.failures) |
4368 | + self.assertEqual(4, filtered_result.testsRun) |
4369 | + |
4370 | + def test_fixup_expected_errors(self): |
4371 | + filtered_result = unittest.TestResult() |
4372 | + result_filter = TestResultFilter(filtered_result, |
4373 | + fixup_expected_failures=set(["error"])) |
4374 | + self.run_tests(result_filter) |
4375 | + self.assertEqual(['error', 'todo'], |
4376 | + [failure[0].id() for failure in filtered_result.expectedFailures]) |
4377 | + self.assertEqual([], filtered_result.errors) |
4378 | + self.assertEqual(4, filtered_result.testsRun) |
4379 | + |
4380 | + def test_fixup_unexpected_success(self): |
4381 | + filtered_result = unittest.TestResult() |
4382 | + result_filter = TestResultFilter(filtered_result, filter_success=False, |
4383 | + fixup_expected_failures=set(["passed"])) |
4384 | + self.run_tests(result_filter) |
4385 | + self.assertEqual(['passed'], |
4386 | + [passed.id() for passed in filtered_result.unexpectedSuccesses]) |
4387 | + self.assertEqual(5, filtered_result.testsRun) |
4388 | + |
4389 | + def test_exclude_failure(self): |
4390 | + filtered_result = unittest.TestResult() |
4391 | + result_filter = TestResultFilter(filtered_result, filter_failure=True) |
4392 | + self.run_tests(result_filter) |
4393 | + self.assertEqual(['error'], |
4394 | + [error[0].id() for error in filtered_result.errors]) |
4395 | + self.assertEqual([], |
4396 | + [failure[0].id() for failure in |
4397 | + filtered_result.failures]) |
4398 | + self.assertEqual(3, filtered_result.testsRun) |
4399 | + |
4400 | + def test_exclude_skips(self): |
4401 | + filtered_result = subunit.TestResultStats(None) |
4402 | + result_filter = TestResultFilter(filtered_result, filter_skip=True) |
4403 | + self.run_tests(result_filter) |
4404 | + self.assertEqual(0, filtered_result.skipped_tests) |
4405 | + self.assertEqual(2, filtered_result.failed_tests) |
4406 | + self.assertEqual(3, filtered_result.testsRun) |
4407 | + |
4408 | + def test_include_success(self): |
4409 | + """Successes can be included if requested.""" |
4410 | + filtered_result = unittest.TestResult() |
4411 | + result_filter = TestResultFilter(filtered_result, |
4412 | + filter_success=False) |
4413 | + self.run_tests(result_filter) |
4414 | + self.assertEqual(['error'], |
4415 | + [error[0].id() for error in filtered_result.errors]) |
4416 | + self.assertEqual(['failed'], |
4417 | + [failure[0].id() for failure in |
4418 | + filtered_result.failures]) |
4419 | + self.assertEqual(5, filtered_result.testsRun) |
4420 | + |
4421 | + def test_filter_predicate(self): |
4422 | + """You can filter by predicate callbacks""" |
4423 | + filtered_result = unittest.TestResult() |
4424 | + def filter_cb(test, outcome, err, details): |
4425 | + return outcome == 'success' |
4426 | + result_filter = TestResultFilter(filtered_result, |
4427 | + filter_predicate=filter_cb, |
4428 | + filter_success=False) |
4429 | + self.run_tests(result_filter) |
4430 | + # Only success should pass |
4431 | + self.assertEqual(1, filtered_result.testsRun) |
4432 | + |
4433 | + def test_time_ordering_preserved(self): |
4434 | + # Passing a subunit stream through TestResultFilter preserves the |
4435 | + # relative ordering of 'time' directives and any other subunit |
4436 | + # directives that are still included. |
4437 | + date_a = datetime(year=2000, month=1, day=1, tzinfo=iso8601.UTC) |
4438 | + date_b = datetime(year=2000, month=1, day=2, tzinfo=iso8601.UTC) |
4439 | + date_c = datetime(year=2000, month=1, day=3, tzinfo=iso8601.UTC) |
4440 | + subunit_stream = _b('\n'.join([ |
4441 | + "time: %s", |
4442 | + "test: foo", |
4443 | + "time: %s", |
4444 | + "error: foo", |
4445 | + "time: %s", |
4446 | + ""]) % (date_a, date_b, date_c)) |
4447 | + result = ExtendedTestResult() |
4448 | + result_filter = TestResultFilter(result) |
4449 | + self.run_tests(result_filter, subunit_stream) |
4450 | + foo = subunit.RemotedTestCase('foo') |
4451 | + self.assertEquals( |
4452 | + [('time', date_a), |
4453 | + ('startTest', foo), |
4454 | + ('time', date_b), |
4455 | + ('addError', foo, {}), |
4456 | + ('stopTest', foo), |
4457 | + ('time', date_c)], result._events) |
4458 | + |
4459 | + def test_skip_preserved(self): |
4460 | + subunit_stream = _b('\n'.join([ |
4461 | + "test: foo", |
4462 | + "skip: foo", |
4463 | + ""])) |
4464 | + result = ExtendedTestResult() |
4465 | + result_filter = TestResultFilter(result) |
4466 | + self.run_tests(result_filter, subunit_stream) |
4467 | + foo = subunit.RemotedTestCase('foo') |
4468 | + self.assertEquals( |
4469 | + [('startTest', foo), |
4470 | + ('addSkip', foo, {}), |
4471 | + ('stopTest', foo), ], result._events) |
4472 | + |
4473 | + |
4474 | +def test_suite(): |
4475 | + loader = subunit.tests.TestUtil.TestLoader() |
4476 | + result = loader.loadTestsFromName(__name__) |
4477 | + return result |
4478 | |
4479 | === added file 'test/python/subunit/tests/test_subunit_stats.py' |
4480 | --- test/python/subunit/tests/test_subunit_stats.py 1970-01-01 00:00:00 +0000 |
4481 | +++ test/python/subunit/tests/test_subunit_stats.py 2011-06-10 09:54:56 +0000 |
4482 | @@ -0,0 +1,84 @@ |
4483 | +# |
4484 | +# subunit: extensions to python unittest to get test results from subprocesses. |
4485 | +# Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> |
4486 | +# |
4487 | +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause |
4488 | +# license at the users choice. A copy of both licenses are available in the |
4489 | +# project source as Apache-2.0 and BSD. You may not use this file except in |
4490 | +# compliance with one of these two licences. |
4491 | +# |
4492 | +# Unless required by applicable law or agreed to in writing, software |
4493 | +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT |
4494 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
4495 | +# license you chose for the specific language governing permissions and |
4496 | +# limitations under that license. |
4497 | +# |
4498 | + |
4499 | +"""Tests for subunit.TestResultStats.""" |
4500 | + |
4501 | +import unittest |
4502 | + |
4503 | +from testtools.compat import _b, BytesIO, StringIO |
4504 | + |
4505 | +import subunit |
4506 | + |
4507 | + |
4508 | +class TestTestResultStats(unittest.TestCase): |
4509 | + """Test for TestResultStats, a TestResult object that generates stats.""" |
4510 | + |
4511 | + def setUp(self): |
4512 | + self.output = StringIO() |
4513 | + self.result = subunit.TestResultStats(self.output) |
4514 | + self.input_stream = BytesIO() |
4515 | + self.test = subunit.ProtocolTestCase(self.input_stream) |
4516 | + |
4517 | + def test_stats_empty(self): |
4518 | + self.test.run(self.result) |
4519 | + self.assertEqual(0, self.result.total_tests) |
4520 | + self.assertEqual(0, self.result.passed_tests) |
4521 | + self.assertEqual(0, self.result.failed_tests) |
4522 | + self.assertEqual(set(), self.result.seen_tags) |
4523 | + |
4524 | + def setUpUsedStream(self): |
4525 | + self.input_stream.write(_b("""tags: global |
4526 | +test passed |
4527 | +success passed |
4528 | +test failed |
4529 | +tags: local |
4530 | +failure failed |
4531 | +test error |
4532 | +error error |
4533 | +test skipped |
4534 | +skip skipped |
4535 | +test todo |
4536 | +xfail todo |
4537 | +""")) |
4538 | + self.input_stream.seek(0) |
4539 | + self.test.run(self.result) |
4540 | + |
4541 | + def test_stats_smoke_everything(self): |
4542 | + # Statistics are calculated usefully. |
4543 | + self.setUpUsedStream() |
4544 | + self.assertEqual(5, self.result.total_tests) |
4545 | + self.assertEqual(2, self.result.passed_tests) |
4546 | + self.assertEqual(2, self.result.failed_tests) |
4547 | + self.assertEqual(1, self.result.skipped_tests) |
4548 | + self.assertEqual(set(["global", "local"]), self.result.seen_tags) |
4549 | + |
4550 | + def test_stat_formatting(self): |
4551 | + expected = (""" |
4552 | +Total tests: 5 |
4553 | +Passed tests: 2 |
4554 | +Failed tests: 2 |
4555 | +Skipped tests: 1 |
4556 | +Seen tags: global, local |
4557 | +""")[1:] |
4558 | + self.setUpUsedStream() |
4559 | + self.result.formatStats() |
4560 | + self.assertEqual(expected, self.output.getvalue()) |
4561 | + |
4562 | + |
4563 | +def test_suite(): |
4564 | + loader = subunit.tests.TestUtil.TestLoader() |
4565 | + result = loader.loadTestsFromName(__name__) |
4566 | + return result |
4567 | |
4568 | === added file 'test/python/subunit/tests/test_subunit_tags.py' |
4569 | --- test/python/subunit/tests/test_subunit_tags.py 1970-01-01 00:00:00 +0000 |
4570 | +++ test/python/subunit/tests/test_subunit_tags.py 2011-06-10 09:54:56 +0000 |
4571 | @@ -0,0 +1,69 @@ |
4572 | +# |
4573 | +# subunit: extensions to python unittest to get test results from subprocesses. |
4574 | +# Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> |
4575 | +# |
4576 | +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause |
4577 | +# license at the users choice. A copy of both licenses are available in the |
4578 | +# project source as Apache-2.0 and BSD. You may not use this file except in |
4579 | +# compliance with one of these two licences. |
4580 | +# |
4581 | +# Unless required by applicable law or agreed to in writing, software |
4582 | +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT |
4583 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
4584 | +# license you chose for the specific language governing permissions and |
4585 | +# limitations under that license. |
4586 | +# |
4587 | + |
4588 | +"""Tests for subunit.tag_stream.""" |
4589 | + |
4590 | +import unittest |
4591 | + |
4592 | +from testtools.compat import StringIO |
4593 | + |
4594 | +import subunit |
4595 | +import subunit.test_results |
4596 | + |
4597 | + |
4598 | +class TestSubUnitTags(unittest.TestCase): |
4599 | + |
4600 | + def setUp(self): |
4601 | + self.original = StringIO() |
4602 | + self.filtered = StringIO() |
4603 | + |
4604 | + def test_add_tag(self): |
4605 | + self.original.write("tags: foo\n") |
4606 | + self.original.write("test: test\n") |
4607 | + self.original.write("tags: bar -quux\n") |
4608 | + self.original.write("success: test\n") |
4609 | + self.original.seek(0) |
4610 | + result = subunit.tag_stream(self.original, self.filtered, ["quux"]) |
4611 | + self.assertEqual([ |
4612 | + "tags: quux", |
4613 | + "tags: foo", |
4614 | + "test: test", |
4615 | + "tags: bar", |
4616 | + "success: test", |
4617 | + ], |
4618 | + self.filtered.getvalue().splitlines()) |
4619 | + |
4620 | + def test_remove_tag(self): |
4621 | + self.original.write("tags: foo\n") |
4622 | + self.original.write("test: test\n") |
4623 | + self.original.write("tags: bar -quux\n") |
4624 | + self.original.write("success: test\n") |
4625 | + self.original.seek(0) |
4626 | + result = subunit.tag_stream(self.original, self.filtered, ["-bar"]) |
4627 | + self.assertEqual([ |
4628 | + "tags: -bar", |
4629 | + "tags: foo", |
4630 | + "test: test", |
4631 | + "tags: -quux", |
4632 | + "success: test", |
4633 | + ], |
4634 | + self.filtered.getvalue().splitlines()) |
4635 | + |
4636 | + |
4637 | +def test_suite(): |
4638 | + loader = subunit.tests.TestUtil.TestLoader() |
4639 | + result = loader.loadTestsFromName(__name__) |
4640 | + return result |
4641 | |
4642 | === added file 'test/python/subunit/tests/test_tap2subunit.py' |
4643 | --- test/python/subunit/tests/test_tap2subunit.py 1970-01-01 00:00:00 +0000 |
4644 | +++ test/python/subunit/tests/test_tap2subunit.py 2011-06-10 09:54:56 +0000 |
4645 | @@ -0,0 +1,445 @@ |
4646 | +# |
4647 | +# subunit: extensions to python unittest to get test results from subprocesses. |
4648 | +# Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> |
4649 | +# |
4650 | +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause |
4651 | +# license at the users choice. A copy of both licenses are available in the |
4652 | +# project source as Apache-2.0 and BSD. You may not use this file except in |
4653 | +# compliance with one of these two licences. |
4654 | +# |
4655 | +# Unless required by applicable law or agreed to in writing, software |
4656 | +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT |
4657 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
4658 | +# license you chose for the specific language governing permissions and |
4659 | +# limitations under that license. |
4660 | +# |
4661 | + |
4662 | +"""Tests for TAP2SubUnit.""" |
4663 | + |
4664 | +import unittest |
4665 | + |
4666 | +from testtools.compat import StringIO |
4667 | + |
4668 | +import subunit |
4669 | + |
4670 | + |
4671 | +class TestTAP2SubUnit(unittest.TestCase): |
4672 | + """Tests for TAP2SubUnit. |
4673 | + |
4674 | + These tests test TAP string data in, and subunit string data out. |
4675 | + This is ok because the subunit protocol is intended to be stable, |
4676 | + but it might be easier/pithier to write tests against TAP string in, |
4677 | + parsed subunit objects out (by hooking the subunit stream to a subunit |
4678 | + protocol server. |
4679 | + """ |
4680 | + |
4681 | + def setUp(self): |
4682 | + self.tap = StringIO() |
4683 | + self.subunit = StringIO() |
4684 | + |
4685 | + def test_skip_entire_file(self): |
4686 | + # A file |
4687 | + # 1..- # Skipped: comment |
4688 | + # results in a single skipped test. |
4689 | + self.tap.write("1..0 # Skipped: entire file skipped\n") |
4690 | + self.tap.seek(0) |
4691 | + result = subunit.TAP2SubUnit(self.tap, self.subunit) |
4692 | + self.assertEqual(0, result) |
4693 | + self.assertEqual([ |
4694 | + "test file skip", |
4695 | + "skip file skip [", |
4696 | + "Skipped: entire file skipped", |
4697 | + "]", |
4698 | + ], |
4699 | + self.subunit.getvalue().splitlines()) |
4700 | + |
4701 | + def test_ok_test_pass(self): |
4702 | + # A file |
4703 | + # ok |
4704 | + # results in a passed test with name 'test 1' (a synthetic name as tap |
4705 | + # does not require named fixtures - it is the first test in the tap |
4706 | + # stream). |
4707 | + self.tap.write("ok\n") |
4708 | + self.tap.seek(0) |
4709 | + result = subunit.TAP2SubUnit(self.tap, self.subunit) |
4710 | + self.assertEqual(0, result) |
4711 | + self.assertEqual([ |
4712 | + "test test 1", |
4713 | + "success test 1", |
4714 | + ], |
4715 | + self.subunit.getvalue().splitlines()) |
4716 | + |
4717 | + def test_ok_test_number_pass(self): |
4718 | + # A file |
4719 | + # ok 1 |
4720 | + # results in a passed test with name 'test 1' |
4721 | + self.tap.write("ok 1\n") |
4722 | + self.tap.seek(0) |
4723 | + result = subunit.TAP2SubUnit(self.tap, self.subunit) |
4724 | + self.assertEqual(0, result) |
4725 | + self.assertEqual([ |
4726 | + "test test 1", |
4727 | + "success test 1", |
4728 | + ], |
4729 | + self.subunit.getvalue().splitlines()) |
4730 | + |
4731 | + def test_ok_test_number_description_pass(self): |
4732 | + # A file |
4733 | + # ok 1 - There is a description |
4734 | + # results in a passed test with name 'test 1 - There is a description' |
4735 | + self.tap.write("ok 1 - There is a description\n") |
4736 | + self.tap.seek(0) |
4737 | + result = subunit.TAP2SubUnit(self.tap, self.subunit) |
4738 | + self.assertEqual(0, result) |
4739 | + self.assertEqual([ |
4740 | + "test test 1 - There is a description", |
4741 | + "success test 1 - There is a description", |
4742 | + ], |
4743 | + self.subunit.getvalue().splitlines()) |
4744 | + |
4745 | + def test_ok_test_description_pass(self): |
4746 | + # A file |
4747 | + # ok There is a description |
4748 | + # results in a passed test with name 'test 1 There is a description' |
4749 | + self.tap.write("ok There is a description\n") |
4750 | + self.tap.seek(0) |
4751 | + result = subunit.TAP2SubUnit(self.tap, self.subunit) |
4752 | + self.assertEqual(0, result) |
4753 | + self.assertEqual([ |
4754 | + "test test 1 There is a description", |
4755 | + "success test 1 There is a description", |
4756 | + ], |
4757 | + self.subunit.getvalue().splitlines()) |
4758 | + |
4759 | + def test_ok_SKIP_skip(self): |
4760 | + # A file |
4761 | + # ok # SKIP |
4762 | + # results in a skkip test with name 'test 1' |
4763 | + self.tap.write("ok # SKIP\n") |
4764 | + self.tap.seek(0) |
4765 | + result = subunit.TAP2SubUnit(self.tap, self.subunit) |
4766 | + self.assertEqual(0, result) |
4767 | + self.assertEqual([ |
4768 | + "test test 1", |
4769 | + "skip test 1", |
4770 | + ], |
4771 | + self.subunit.getvalue().splitlines()) |
4772 | + |
4773 | + def test_ok_skip_number_comment_lowercase(self): |
4774 | + self.tap.write("ok 1 # skip no samba environment available, skipping compilation\n") |
4775 | + self.tap.seek(0) |
4776 | + result = subunit.TAP2SubUnit(self.tap, self.subunit) |
4777 | + self.assertEqual(0, result) |
4778 | + self.assertEqual([ |
4779 | + "test test 1", |
4780 | + "skip test 1 [", |
4781 | + "no samba environment available, skipping compilation", |
4782 | + "]" |
4783 | + ], |
4784 | + self.subunit.getvalue().splitlines()) |
4785 | + |
4786 | + def test_ok_number_description_SKIP_skip_comment(self): |
4787 | + # A file |
4788 | + # ok 1 foo # SKIP Not done yet |
4789 | + # results in a skip test with name 'test 1 foo' and a log of |
4790 | + # Not done yet |
4791 | + self.tap.write("ok 1 foo # SKIP Not done yet\n") |
4792 | + self.tap.seek(0) |
4793 | + result = subunit.TAP2SubUnit(self.tap, self.subunit) |
4794 | + self.assertEqual(0, result) |
4795 | + self.assertEqual([ |
4796 | + "test test 1 foo", |
4797 | + "skip test 1 foo [", |
4798 | + "Not done yet", |
4799 | + "]", |
4800 | + ], |
4801 | + self.subunit.getvalue().splitlines()) |
4802 | + |
4803 | + def test_ok_SKIP_skip_comment(self): |
4804 | + # A file |
4805 | + # ok # SKIP Not done yet |
4806 | + # results in a skip test with name 'test 1' and a log of Not done yet |
4807 | + self.tap.write("ok # SKIP Not done yet\n") |
4808 | + self.tap.seek(0) |
4809 | + result = subunit.TAP2SubUnit(self.tap, self.subunit) |
4810 | + self.assertEqual(0, result) |
4811 | + self.assertEqual([ |
4812 | + "test test 1", |
4813 | + "skip test 1 [", |
4814 | + "Not done yet", |
4815 | + "]", |
4816 | + ], |
4817 | + self.subunit.getvalue().splitlines()) |
4818 | + |
4819 | + def test_ok_TODO_xfail(self): |
4820 | + # A file |
4821 | + # ok # TODO |
4822 | + # results in a xfail test with name 'test 1' |
4823 | + self.tap.write("ok # TODO\n") |
4824 | + self.tap.seek(0) |
4825 | + result = subunit.TAP2SubUnit(self.tap, self.subunit) |
4826 | + self.assertEqual(0, result) |
4827 | + self.assertEqual([ |
4828 | + "test test 1", |
4829 | + "xfail test 1", |
4830 | + ], |
4831 | + self.subunit.getvalue().splitlines()) |
4832 | + |
4833 | + def test_ok_TODO_xfail_comment(self): |
4834 | + # A file |
4835 | + # ok # TODO Not done yet |
4836 | + # results in a xfail test with name 'test 1' and a log of Not done yet |
4837 | + self.tap.write("ok # TODO Not done yet\n") |
4838 | + self.tap.seek(0) |
4839 | + result = subunit.TAP2SubUnit(self.tap, self.subunit) |
4840 | + self.assertEqual(0, result) |
4841 | + self.assertEqual([ |
4842 | + "test test 1", |
4843 | + "xfail test 1 [", |
4844 | + "Not done yet", |
4845 | + "]", |
4846 | + ], |
4847 | + self.subunit.getvalue().splitlines()) |
4848 | + |
4849 | + def test_bail_out_errors(self): |
4850 | + # A file with line in it |
4851 | + # Bail out! COMMENT |
4852 | + # is treated as an error |
4853 | + self.tap.write("ok 1 foo\n") |
4854 | + self.tap.write("Bail out! Lifejacket engaged\n") |
4855 | + self.tap.seek(0) |
4856 | + result = subunit.TAP2SubUnit(self.tap, self.subunit) |
4857 | + self.assertEqual(0, result) |
4858 | + self.assertEqual([ |
4859 | + "test test 1 foo", |
4860 | + "success test 1 foo", |
4861 | + "test Bail out! Lifejacket engaged", |
4862 | + "error Bail out! Lifejacket engaged", |
4863 | + ], |
4864 | + self.subunit.getvalue().splitlines()) |
4865 | + |
4866 | + def test_missing_test_at_end_with_plan_adds_error(self): |
4867 | + # A file |
4868 | + # 1..3 |
4869 | + # ok first test |
4870 | + # not ok third test |
4871 | + # results in three tests, with the third being created |
4872 | + self.tap.write('1..3\n') |
4873 | + self.tap.write('ok first test\n') |
4874 | + self.tap.write('not ok second test\n') |
4875 | + self.tap.seek(0) |
4876 | + result = subunit.TAP2SubUnit(self.tap, self.subunit) |
4877 | + self.assertEqual(0, result) |
4878 | + self.assertEqual([ |
4879 | + 'test test 1 first test', |
4880 | + 'success test 1 first test', |
4881 | + 'test test 2 second test', |
4882 | + 'failure test 2 second test', |
4883 | + 'test test 3', |
4884 | + 'error test 3 [', |
4885 | + 'test missing from TAP output', |
4886 | + ']', |
4887 | + ], |
4888 | + self.subunit.getvalue().splitlines()) |
4889 | + |
4890 | + def test_missing_test_with_plan_adds_error(self): |
4891 | + # A file |
4892 | + # 1..3 |
4893 | + # ok first test |
4894 | + # not ok 3 third test |
4895 | + # results in three tests, with the second being created |
4896 | + self.tap.write('1..3\n') |
4897 | + self.tap.write('ok first test\n') |
4898 | + self.tap.write('not ok 3 third test\n') |
4899 | + self.tap.seek(0) |
4900 | + result = subunit.TAP2SubUnit(self.tap, self.subunit) |
4901 | + self.assertEqual(0, result) |
4902 | + self.assertEqual([ |
4903 | + 'test test 1 first test', |
4904 | + 'success test 1 first test', |
4905 | + 'test test 2', |
4906 | + 'error test 2 [', |
4907 | + 'test missing from TAP output', |
4908 | + ']', |
4909 | + 'test test 3 third test', |
4910 | + 'failure test 3 third test', |
4911 | + ], |
4912 | + self.subunit.getvalue().splitlines()) |
4913 | + |
4914 | + def test_missing_test_no_plan_adds_error(self): |
4915 | + # A file |
4916 | + # ok first test |
4917 | + # not ok 3 third test |
4918 | + # results in three tests, with the second being created |
4919 | + self.tap.write('ok first test\n') |
4920 | + self.tap.write('not ok 3 third test\n') |
4921 | + self.tap.seek(0) |
4922 | + result = subunit.TAP2SubUnit(self.tap, self.subunit) |
4923 | + self.assertEqual(0, result) |
4924 | + self.assertEqual([ |
4925 | + 'test test 1 first test', |
4926 | + 'success test 1 first test', |
4927 | + 'test test 2', |
4928 | + 'error test 2 [', |
4929 | + 'test missing from TAP output', |
4930 | + ']', |
4931 | + 'test test 3 third test', |
4932 | + 'failure test 3 third test', |
4933 | + ], |
4934 | + self.subunit.getvalue().splitlines()) |
4935 | + |
4936 | + def test_four_tests_in_a_row_trailing_plan(self): |
4937 | + # A file |
4938 | + # ok 1 - first test in a script with no plan at all |
4939 | + # not ok 2 - second |
4940 | + # ok 3 - third |
4941 | + # not ok 4 - fourth |
4942 | + # 1..4 |
4943 | + # results in four tests numbered and named |
4944 | + self.tap.write('ok 1 - first test in a script with trailing plan\n') |
4945 | + self.tap.write('not ok 2 - second\n') |
4946 | + self.tap.write('ok 3 - third\n') |
4947 | + self.tap.write('not ok 4 - fourth\n') |
4948 | + self.tap.write('1..4\n') |
4949 | + self.tap.seek(0) |
4950 | + result = subunit.TAP2SubUnit(self.tap, self.subunit) |
4951 | + self.assertEqual(0, result) |
4952 | + self.assertEqual([ |
4953 | + 'test test 1 - first test in a script with trailing plan', |
4954 | + 'success test 1 - first test in a script with trailing plan', |
4955 | + 'test test 2 - second', |
4956 | + 'failure test 2 - second', |
4957 | + 'test test 3 - third', |
4958 | + 'success test 3 - third', |
4959 | + 'test test 4 - fourth', |
4960 | + 'failure test 4 - fourth' |
4961 | + ], |
4962 | + self.subunit.getvalue().splitlines()) |
4963 | + |
4964 | + def test_four_tests_in_a_row_with_plan(self): |
4965 | + # A file |
4966 | + # 1..4 |
4967 | + # ok 1 - first test in a script with no plan at all |
4968 | + # not ok 2 - second |
4969 | + # ok 3 - third |
4970 | + # not ok 4 - fourth |
4971 | + # results in four tests numbered and named |
4972 | + self.tap.write('1..4\n') |
4973 | + self.tap.write('ok 1 - first test in a script with a plan\n') |
4974 | + self.tap.write('not ok 2 - second\n') |
4975 | + self.tap.write('ok 3 - third\n') |
4976 | + self.tap.write('not ok 4 - fourth\n') |
4977 | + self.tap.seek(0) |
4978 | + result = subunit.TAP2SubUnit(self.tap, self.subunit) |
4979 | + self.assertEqual(0, result) |
4980 | + self.assertEqual([ |
4981 | + 'test test 1 - first test in a script with a plan', |
4982 | + 'success test 1 - first test in a script with a plan', |
4983 | + 'test test 2 - second', |
4984 | + 'failure test 2 - second', |
4985 | + 'test test 3 - third', |
4986 | + 'success test 3 - third', |
4987 | + 'test test 4 - fourth', |
4988 | + 'failure test 4 - fourth' |
4989 | + ], |
4990 | + self.subunit.getvalue().splitlines()) |
4991 | + |
4992 | + def test_four_tests_in_a_row_no_plan(self): |
4993 | + # A file |
4994 | + # ok 1 - first test in a script with no plan at all |
4995 | + # not ok 2 - second |
4996 | + # ok 3 - third |
4997 | + # not ok 4 - fourth |
4998 | + # results in four tests numbered and named |
4999 | + self.tap.write('ok 1 - first test in a script with no plan at all\n') |
5000 | + self.tap.write('not ok 2 - second\n') |
The diff has been truncated for viewing.