Compare commits
894 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e84c00efae | ||
|
|
53021ea264 | ||
|
|
01d067ec2b | ||
|
|
cdd25c9512 | ||
|
|
898b63b665 | ||
|
|
080dfb9841 | ||
|
|
02421790bf | ||
|
|
48d91def7e | ||
|
|
eb73db56c7 | ||
|
|
26f590babe | ||
|
|
f90b2f845c | ||
|
|
9d4e0365da | ||
|
|
0431b8bb74 | ||
|
|
2653024409 | ||
|
|
923174718e | ||
|
|
73f37d0989 | ||
|
|
0722b95e53 | ||
|
|
ae89436d97 | ||
|
|
af77a23501 | ||
|
|
2a1424e563 | ||
|
|
1871d526ac | ||
|
|
9346e18d8c | ||
|
|
1db5c95414 | ||
|
|
0c05b906d4 | ||
|
|
f04e01f55f | ||
|
|
ca44e88e54 | ||
|
|
b5fd3cfb84 | ||
|
|
dc727832a0 | ||
|
|
bf837164b4 | ||
|
|
f6d589caa1 | ||
|
|
738c8762df | ||
|
|
3d990a6237 | ||
|
|
b024dbe794 | ||
|
|
83a93e5e9a | ||
|
|
504b6b72a9 | ||
|
|
45851644c1 | ||
|
|
5a06330f61 | ||
|
|
11ad61ba07 | ||
|
|
129549fd5b | ||
|
|
652a353319 | ||
|
|
d6d8d5b9d1 | ||
|
|
7f554f50e3 | ||
|
|
e94f5727c4 | ||
|
|
35da487d46 | ||
|
|
a4ddec1157 | ||
|
|
b4a655af52 | ||
|
|
0cc9d7e6b6 | ||
|
|
6fe5493c3c | ||
|
|
a3aebfaefe | ||
|
|
9d5182eaad | ||
|
|
a6b5f583a7 | ||
|
|
cfe9c9c169 | ||
|
|
0b361c62c8 | ||
|
|
282f4ce920 | ||
|
|
6e2bc7712c | ||
|
|
173bd13ece | ||
|
|
e04273df57 | ||
|
|
15328c04eb | ||
|
|
6591d7a209 | ||
|
|
220d177e76 | ||
|
|
9cae9a2a97 | ||
|
|
129bb5393b | ||
|
|
db08dcab14 | ||
|
|
edd6c0f9b3 | ||
|
|
a58cb3b95c | ||
|
|
40a682476d | ||
|
|
b7d43c5a5d | ||
|
|
236cac86b1 | ||
|
|
79c0515945 | ||
|
|
0e916460d8 | ||
|
|
69a0eacb4f | ||
|
|
7f05cd8bfc | ||
|
|
b55d66d0cf | ||
|
|
3ed3e51997 | ||
|
|
81d7883884 | ||
|
|
ad137d46a2 | ||
|
|
e16983d265 | ||
|
|
fd6ff9bc01 | ||
|
|
d2a8866bce | ||
|
|
c69978fbb0 | ||
|
|
c1eaa72883 | ||
|
|
55f3ffd2ba | ||
|
|
55eb82c434 | ||
|
|
d94a29e866 | ||
|
|
a7d2a82caa | ||
|
|
ce95437dee | ||
|
|
3ba3112a85 | ||
|
|
c019e489d2 | ||
|
|
43e4fcf6dd | ||
|
|
65ca554230 | ||
|
|
52a2999a91 | ||
|
|
f3580bee2d | ||
|
|
deb163d237 | ||
|
|
6f81602ba2 | ||
|
|
ac17f20d98 | ||
|
|
a9b7de8bf0 | ||
|
|
0fc75c9622 | ||
|
|
9726fafa98 | ||
|
|
cf7bb70809 | ||
|
|
03c3930734 | ||
|
|
25a4d7d882 | ||
|
|
eead0365b5 | ||
|
|
eb3b989286 | ||
|
|
cdb46e9f16 | ||
|
|
ead25634fe | ||
|
|
3f3521c826 | ||
|
|
0c2dfb9683 | ||
|
|
5d6b0a59c0 | ||
|
|
c629f6b18b | ||
|
|
b48eda6d7d | ||
|
|
81243657be | ||
|
|
24fe051803 | ||
|
|
16bcfa749e | ||
|
|
d083153548 | ||
|
|
41e6b04f0b | ||
|
|
33c2a3a3e5 | ||
|
|
97d5da5c99 | ||
|
|
d1005ebb8f | ||
|
|
c4623939af | ||
|
|
060609317a | ||
|
|
bae0f7f46a | ||
|
|
09a50b6e6d | ||
|
|
200fe072f1 | ||
|
|
ff9ec13ddb | ||
|
|
9384ff38c9 | ||
|
|
1e6e373913 | ||
|
|
e510eb0d45 | ||
|
|
2e2d6a8bef | ||
|
|
44cd1c9862 | ||
|
|
f6f084fa01 | ||
|
|
b7d212c0c9 | ||
|
|
bda3e53df6 | ||
|
|
9d4354c0a4 | ||
|
|
f99ddbfdf2 | ||
|
|
019dc14fc4 | ||
|
|
dbb58b39d9 | ||
|
|
c3ca44b46f | ||
|
|
f6df3b0b97 | ||
|
|
e9d4853296 | ||
|
|
28c2327f73 | ||
|
|
8ed5b77aba | ||
|
|
660b84a052 | ||
|
|
d73e689991 | ||
|
|
c773ea664b | ||
|
|
aa757f7715 | ||
|
|
4222a806f1 | ||
|
|
415c76b255 | ||
|
|
f1c9554f42 | ||
|
|
3d0b756877 | ||
|
|
ea4e9fa4e2 | ||
|
|
777bde7c60 | ||
|
|
e19f3c260f | ||
|
|
5025478ac2 | ||
|
|
5b73de92c2 | ||
|
|
6a237bd49f | ||
|
|
89710b4901 | ||
|
|
459d6e610c | ||
|
|
d1adbf4a5c | ||
|
|
c11cc36997 | ||
|
|
2b7fae2368 | ||
|
|
f2ca0b8170 | ||
|
|
912c8f0540 | ||
|
|
8f12269db7 | ||
|
|
7325a5fe2e | ||
|
|
9f4d0be895 | ||
|
|
394367e1d2 | ||
|
|
7e15fb7f2d | ||
|
|
8859936301 | ||
|
|
d580f5bac8 | ||
|
|
5941b2e071 | ||
|
|
bca19a1156 | ||
|
|
3df5989326 | ||
|
|
98b9de4ff1 | ||
|
|
0e248c9aed | ||
|
|
883b3ca2c1 | ||
|
|
e4bb48995a | ||
|
|
1d15bb2880 | ||
|
|
959395b796 | ||
|
|
f6caf230f8 | ||
|
|
f2cdbe776e | ||
|
|
97c9c10f3c | ||
|
|
f74930db79 | ||
|
|
5a88a9b22a | ||
|
|
d0d4759c96 | ||
|
|
749316623b | ||
|
|
60cdb875ed | ||
|
|
e0251ecb41 | ||
|
|
0834b63560 | ||
|
|
0c8569dcb0 | ||
|
|
5f2444d2a2 | ||
|
|
537dca477b | ||
|
|
8480111012 | ||
|
|
c63dc62294 | ||
|
|
8d19ccb56f | ||
|
|
eac4514227 | ||
|
|
16f0d100cc | ||
|
|
0b620c304b | ||
|
|
7371d436d2 | ||
|
|
b928928942 | ||
|
|
4337e9c4ba | ||
|
|
8dfd6c17e3 | ||
|
|
de7c97aa1e | ||
|
|
ace668ae8f | ||
|
|
857db415bc | ||
|
|
f8623a6668 | ||
|
|
4cadc600d5 | ||
|
|
d62ec2985d | ||
|
|
b2341899c5 | ||
|
|
2c84e9ddac | ||
|
|
b7dd8eac8e | ||
|
|
61d0209093 | ||
|
|
8b9bb5ff54 | ||
|
|
cb2c89a070 | ||
|
|
cea9367739 | ||
|
|
e1a66d54b9 | ||
|
|
a37f9f19d4 | ||
|
|
91aa6ab0df | ||
|
|
c85c5d05c5 | ||
|
|
34df678bc3 | ||
|
|
8c91ffc701 | ||
|
|
eda39f361d | ||
|
|
c58770bfef | ||
|
|
d9858844c3 | ||
|
|
04b8111f8f | ||
|
|
0253f7b8d5 | ||
|
|
5999368002 | ||
|
|
bf2f2dc2a6 | ||
|
|
787b0212d1 | ||
|
|
1de5af66da | ||
|
|
61caa4f776 | ||
|
|
9ca7e46a0a | ||
|
|
f66e0825b2 | ||
|
|
f5f924d293 | ||
|
|
c3d1986101 | ||
|
|
89de87dce1 | ||
|
|
ab005a4261 | ||
|
|
3d6ad054c0 | ||
|
|
b6e619413f | ||
|
|
a43fb9cd93 | ||
|
|
68f3818562 | ||
|
|
1ed1ef3c71 | ||
|
|
b6475b058f | ||
|
|
e39556ada0 | ||
|
|
966c63d477 | ||
|
|
a298077461 | ||
|
|
e1aed27c15 | ||
|
|
a759da0208 | ||
|
|
6ab36592ea | ||
|
|
c45b7012f5 | ||
|
|
1d10db4bab | ||
|
|
767e44ef29 | ||
|
|
4a11edfb68 | ||
|
|
e53bb1af45 | ||
|
|
e5d0862fa5 | ||
|
|
2ed4c2135a | ||
|
|
8cfec56a82 | ||
|
|
818a412d29 | ||
|
|
39158957f4 | ||
|
|
2cb0145bce | ||
|
|
dfda9acd6d | ||
|
|
c6951d5184 | ||
|
|
f85648bbf8 | ||
|
|
dd1ee22293 | ||
|
|
9def8522f3 | ||
|
|
a999fcd36a | ||
|
|
24468a6f34 | ||
|
|
ba8ae427e2 | ||
|
|
d6d2e6c615 | ||
|
|
7f93063945 | ||
|
|
63f070317c | ||
|
|
3d84f35850 | ||
|
|
3d794b6b38 | ||
|
|
d8f4663f49 | ||
|
|
34b9f90f35 | ||
|
|
5ec1dd2609 | ||
|
|
b691cbd7e2 | ||
|
|
d7ada40130 | ||
|
|
2f141bbc54 | ||
|
|
a098226ee4 | ||
|
|
fd2572a39d | ||
|
|
05d4a3f9eb | ||
|
|
2161b54555 | ||
|
|
69ff29bf44 | ||
|
|
de83d35994 | ||
|
|
c7c4f62f77 | ||
|
|
e635f9f9b2 | ||
|
|
f250e912eb | ||
|
|
351931d5ca | ||
|
|
28c785a0d1 | ||
|
|
3de715ec13 | ||
|
|
ea5fb0c153 | ||
|
|
9e549a1acf | ||
|
|
c7a45d6eaf | ||
|
|
37631dbfa0 | ||
|
|
e9f240a9a3 | ||
|
|
531be22a87 | ||
|
|
be93fdf11d | ||
|
|
f05cb934a9 | ||
|
|
82d806deb6 | ||
|
|
72e6f55b45 | ||
|
|
9a0f2a9fb7 | ||
|
|
49b7237581 | ||
|
|
814d348e7d | ||
|
|
88c14cad94 | ||
|
|
b57545bd21 | ||
|
|
2eef674615 | ||
|
|
bf7c5ea32c | ||
|
|
d99243c1a7 | ||
|
|
62b8712ca9 | ||
|
|
68105b3ae4 | ||
|
|
be503f1c43 | ||
|
|
5abca55412 | ||
|
|
79d2edcbff | ||
|
|
e2c6a77f06 | ||
|
|
35eec99d41 | ||
|
|
e4fde1048b | ||
|
|
bc4eecbbac | ||
|
|
1408c9f077 | ||
|
|
418607846a | ||
|
|
6aa5611ae5 | ||
|
|
c5c9fb93aa | ||
|
|
86961291e5 | ||
|
|
7c1dadee51 | ||
|
|
b16553e34e | ||
|
|
b6dcfd4377 | ||
|
|
1a80487e71 | ||
|
|
488720da8d | ||
|
|
0264121c10 | ||
|
|
d2f448ecee | ||
|
|
c692a0ee9c | ||
|
|
8844d9d04f | ||
|
|
4063b7f8e0 | ||
|
|
a8dfe34bfb | ||
|
|
7d9d502a01 | ||
|
|
09a44f4cac | ||
|
|
b14b9515a5 | ||
|
|
2a504a5bcd | ||
|
|
dfcdf644fe | ||
|
|
7dad3cb157 | ||
|
|
745737e337 | ||
|
|
eae1055fb0 | ||
|
|
2e1c36bbb6 | ||
|
|
e3cea41dcd | ||
|
|
c0f091d540 | ||
|
|
d4cd1aad8e | ||
|
|
39a297afe6 | ||
|
|
068548f7a9 | ||
|
|
0c163ce624 | ||
|
|
7760cf1881 | ||
|
|
37bd1e03cb | ||
|
|
fc95877622 | ||
|
|
03b694a1d0 | ||
|
|
79c2a47985 | ||
|
|
9289d77a80 | ||
|
|
2eee2d0099 | ||
|
|
0ea46e6aef | ||
|
|
54e00429e4 | ||
|
|
b0ac3581dd | ||
|
|
e7ed45a5d4 | ||
|
|
d1bde69c1e | ||
|
|
7cf859085e | ||
|
|
424479cf0f | ||
|
|
98dcd764bc | ||
|
|
ccd67733fb | ||
|
|
5873ca5146 | ||
|
|
d2903507d8 | ||
|
|
224b3a2eda | ||
|
|
5d024c7433 | ||
|
|
9232b88df3 | ||
|
|
e98f77037e | ||
|
|
672e42e558 | ||
|
|
36d7df4542 | ||
|
|
e5eaf02e19 | ||
|
|
c0d1f3f7ef | ||
|
|
9597d3dafe | ||
|
|
f7282b84bd | ||
|
|
1d7b574b31 | ||
|
|
d16fdb378c | ||
|
|
cc092afd3b | ||
|
|
fd4485a540 | ||
|
|
3b8779ad17 | ||
|
|
3e875178ad | ||
|
|
97b671057d | ||
|
|
d6fc489b2b | ||
|
|
4e8438afc8 | ||
|
|
b1f8038abf | ||
|
|
5603a0cd4b | ||
|
|
3c649cf91d | ||
|
|
82d573e391 | ||
|
|
b7b96b24d8 | ||
|
|
990e7bf3b9 | ||
|
|
bcdc3d0154 | ||
|
|
faea7e1407 | ||
|
|
7c701948d5 | ||
|
|
899b804ec1 | ||
|
|
66bd4e485a | ||
|
|
8ff8dd3ae9 | ||
|
|
0d17dc1e19 | ||
|
|
83e0b52294 | ||
|
|
1265612465 | ||
|
|
40eed363e8 | ||
|
|
5ccd3f2fc5 | ||
|
|
ba878c6d9d | ||
|
|
8792261df1 | ||
|
|
c2ed29070a | ||
|
|
38104dfc92 | ||
|
|
2e55c4ba61 | ||
|
|
e6ad6e02d2 | ||
|
|
d08c4ce0ad | ||
|
|
309e3d38a0 | ||
|
|
91e2b23258 | ||
|
|
6a4492a22d | ||
|
|
0dd378da30 | ||
|
|
7b273b8577 | ||
|
|
d98521b0d9 | ||
|
|
52011e84d3 | ||
|
|
de583ed7a3 | ||
|
|
f8480caae4 | ||
|
|
47d9e6ca1f | ||
|
|
6f0a33dfdc | ||
|
|
1641d00cb1 | ||
|
|
2d7a32f7ea | ||
|
|
06acbb9f5e | ||
|
|
7b630d9080 | ||
|
|
98de64badc | ||
|
|
1e241e1f2a | ||
|
|
ff2c18fedb | ||
|
|
bf64a800d6 | ||
|
|
efc57391eb | ||
|
|
dc65aa1fea | ||
|
|
80ad3fb8ed | ||
|
|
bc7110931a | ||
|
|
8c508612ec | ||
|
|
b7d046527e | ||
|
|
42804c52e8 | ||
|
|
d88a3712c5 | ||
|
|
abfedd692e | ||
|
|
d0b048c86d | ||
|
|
b6f069f4c3 | ||
|
|
edc2e5ab82 | ||
|
|
6da9a087f8 | ||
|
|
e19462d581 | ||
|
|
a811fabb43 | ||
|
|
07e76cbef2 | ||
|
|
76d5c9e4f4 | ||
|
|
abcadc4202 | ||
|
|
b7f6a9f3fd | ||
|
|
c8264385ea | ||
|
|
ad8131be9e | ||
|
|
54c88a6cf3 | ||
|
|
115f15600f | ||
|
|
65a145e2a7 | ||
|
|
5719a72eeb | ||
|
|
4bc4495115 | ||
|
|
c66e9f8f0f | ||
|
|
52eafdc21e | ||
|
|
85c0d5481b | ||
|
|
fd9055fd11 | ||
|
|
780bdda95a | ||
|
|
fd4b461290 | ||
|
|
040062e40c | ||
|
|
d853e9167a | ||
|
|
d50ad270f0 | ||
|
|
30c93701a7 | ||
|
|
b507e1754c | ||
|
|
748fce94fd | ||
|
|
d6281b4206 | ||
|
|
b61ed2cf7e | ||
|
|
9263f30c88 | ||
|
|
8f9a88ef7a | ||
|
|
c64af0d9ce | ||
|
|
9181df42da | ||
|
|
74e1a49dd7 | ||
|
|
468b1241a5 | ||
|
|
24744cf5cf | ||
|
|
ffc969b6c2 | ||
|
|
0567a8ee77 | ||
|
|
580c8525f0 | ||
|
|
d6010aa0c9 | ||
|
|
4e35c00ab0 | ||
|
|
c46e2cbbc7 | ||
|
|
c47835f5ec | ||
|
|
412b56f7cf | ||
|
|
faba432996 | ||
|
|
2ba23e8d08 | ||
|
|
d74f852fd6 | ||
|
|
1728798e81 | ||
|
|
53a8d20d88 | ||
|
|
61446faa17 | ||
|
|
9711e335d9 | ||
|
|
080a9d2f12 | ||
|
|
15af7e1662 | ||
|
|
e42cbc714f | ||
|
|
5e26e6e553 | ||
|
|
d0a4d348fe | ||
|
|
494be731e3 | ||
|
|
8ae244a06a | ||
|
|
f91049cec9 | ||
|
|
270d0f89ba | ||
|
|
e382ed4245 | ||
|
|
ef7cb47b1e | ||
|
|
6efde60b8b | ||
|
|
fd059359cc | ||
|
|
c2c504797e | ||
|
|
84f9f45f98 | ||
|
|
28aa4c891e | ||
|
|
6ff0fdb977 | ||
|
|
b0837693d0 | ||
|
|
52851e4388 | ||
|
|
cbe31f3748 | ||
|
|
c9bbdf4f10 | ||
|
|
f984e94fca | ||
|
|
b4fe91943d | ||
|
|
7d6317802e | ||
|
|
0365e5c3a0 | ||
|
|
e6859406f1 | ||
|
|
51cff6f106 | ||
|
|
68e58e1493 | ||
|
|
9f7eac0ba1 | ||
|
|
b0e31dca86 | ||
|
|
7d10a57514 | ||
|
|
2c0f6207e9 | ||
|
|
adb12d0d4f | ||
|
|
844c141d10 | ||
|
|
02d94e69f0 | ||
|
|
1bc56f9838 | ||
|
|
98ea8fae32 | ||
|
|
36288c5134 | ||
|
|
83a3cc9c94 | ||
|
|
0c04b44919 | ||
|
|
a5e8860feb | ||
|
|
8d95f89a6a | ||
|
|
3bca62e9e4 | ||
|
|
72b4534a0c | ||
|
|
21b4280126 | ||
|
|
30a9debaf1 | ||
|
|
4c5718c78d | ||
|
|
c93b949878 | ||
|
|
cf34adb75f | ||
|
|
f824a73143 | ||
|
|
e45a33f029 | ||
|
|
064e79761c | ||
|
|
f7713c47e8 | ||
|
|
8e4e2ba244 | ||
|
|
3b8935c533 | ||
|
|
ce8678e6d5 | ||
|
|
2e1f6c85f6 | ||
|
|
ca5e6830c6 | ||
|
|
69cbac8fb5 | ||
|
|
7301981f32 | ||
|
|
555ba4159d | ||
|
|
f47ae74981 | ||
|
|
e061ace099 | ||
|
|
85d52481b1 | ||
|
|
47379d4a79 | ||
|
|
0cb9d26d83 | ||
|
|
95cc114b34 | ||
|
|
9d716a39d6 | ||
|
|
923dcfd620 | ||
|
|
b5467645d3 | ||
|
|
a65380941d | ||
|
|
17d7c60735 | ||
|
|
81f822d528 | ||
|
|
0b340aa1f6 | ||
|
|
859915dc5e | ||
|
|
a8f4f49a82 | ||
|
|
e18c3ed494 | ||
|
|
2263fcf6b7 | ||
|
|
b96d552dbd | ||
|
|
2b2c1e5b7b | ||
|
|
7b63fa5966 | ||
|
|
b18040337a | ||
|
|
42e0d7970c | ||
|
|
ed0a4fe23b | ||
|
|
bb0632c7ad | ||
|
|
84ab194516 | ||
|
|
530cae9204 | ||
|
|
9dc43e84dc | ||
|
|
8af265da04 | ||
|
|
e3b9382122 | ||
|
|
c3f4eb6d57 | ||
|
|
892aa457be | ||
|
|
8f7b53e55b | ||
|
|
d27c377817 | ||
|
|
50abe43216 | ||
|
|
ddc67ca13a | ||
|
|
a1d3da4027 | ||
|
|
85e7b11ef5 | ||
|
|
77e1f93ca1 | ||
|
|
94b1ce65c6 | ||
|
|
f5b992f68a | ||
|
|
24a458b4c8 | ||
|
|
ac1d277225 | ||
|
|
9777703e03 | ||
|
|
f43cda9681 | ||
|
|
b47fdbe0a7 | ||
|
|
cde970be69 | ||
|
|
cfd43a9b02 | ||
|
|
ebd10aa6b4 | ||
|
|
24db492f53 | ||
|
|
1b387bea62 | ||
|
|
9528b64f7f | ||
|
|
b96559149c | ||
|
|
3388d82c1c | ||
|
|
9985a7cdca | ||
|
|
1d00c5e109 | ||
|
|
0559f11aa5 | ||
|
|
2893cddb68 | ||
|
|
6910641266 | ||
|
|
4b81a07303 | ||
|
|
7eb765578a | ||
|
|
e2cf3e0932 | ||
|
|
1830de2c13 | ||
|
|
25ab906b8b | ||
|
|
8a3b4b9c37 | ||
|
|
2b8a54d5d9 | ||
|
|
4eabfed651 | ||
|
|
505a34bb85 | ||
|
|
2dade6ed00 | ||
|
|
825ea9bfa1 | ||
|
|
cc1186242c | ||
|
|
86284689a3 | ||
|
|
c70d020bf3 | ||
|
|
4622c28ffd | ||
|
|
899998cf9c | ||
|
|
6f385fb4ea | ||
|
|
18e12cbd67 | ||
|
|
3cf4e133cc | ||
|
|
d53bfe0aa7 | ||
|
|
97da43d909 | ||
|
|
ee080ce8a5 | ||
|
|
741a3e8602 | ||
|
|
177637bfb9 | ||
|
|
ae64221c34 | ||
|
|
22017f11d0 | ||
|
|
c1eaad7d57 | ||
|
|
8e6d538a57 | ||
|
|
0836d74ae7 | ||
|
|
a9e5bd52d3 | ||
|
|
b4b5e358c3 | ||
|
|
4190809d1a | ||
|
|
4e2a820c6a | ||
|
|
14a43fffee | ||
|
|
245d39f52d | ||
|
|
ac0b862f8f | ||
|
|
c41db8ebbb | ||
|
|
75c124ea17 | ||
|
|
ab13c3f362 | ||
|
|
c9af19dae1 | ||
|
|
ffffac27f9 | ||
|
|
618bd56acf | ||
|
|
402232e60f | ||
|
|
400b51caf6 | ||
|
|
9aaf0fd340 | ||
|
|
8976b3ee0e | ||
|
|
ac2f2b1deb | ||
|
|
0be961a0f3 | ||
|
|
b4a397d153 | ||
|
|
d1a9ab3df0 | ||
|
|
0ede968ec0 | ||
|
|
5f21abc3a3 | ||
|
|
e2bb81124c | ||
|
|
ea18e9656b | ||
|
|
3cc58c2f78 | ||
|
|
0ac94134f5 | ||
|
|
c142f2551d | ||
|
|
cccfaa81fb | ||
|
|
d960fb78fc | ||
|
|
d02d0bb7b7 | ||
|
|
fe4cdd8a90 | ||
|
|
54a143e6a8 | ||
|
|
3a4f69734a | ||
|
|
fbdc6e8cc0 | ||
|
|
c2c44f0ffc | ||
|
|
e12fe64b54 | ||
|
|
5240252164 | ||
|
|
1ffc006363 | ||
|
|
836232e544 | ||
|
|
2539e5a352 | ||
|
|
8e457338ee | ||
|
|
d92ee8c3c3 | ||
|
|
492c60c202 | ||
|
|
86ef81454f | ||
|
|
675669c52e | ||
|
|
92c5d395a2 | ||
|
|
fa4ea1d9e8 | ||
|
|
657a395839 | ||
|
|
9fb2079458 | ||
|
|
83620ced2e | ||
|
|
1fb824cd28 | ||
|
|
0d35994fb8 | ||
|
|
3a37f33d99 | ||
|
|
ddfb2d5f3a | ||
|
|
d81c0e9a92 | ||
|
|
048cb71bf6 | ||
|
|
903fd144ff | ||
|
|
d51e27a5cb | ||
|
|
064290a606 | ||
|
|
e6ae68c0cc | ||
|
|
43f970ab6b | ||
|
|
49b9f9091a | ||
|
|
99277be25f | ||
|
|
699892bd03 | ||
|
|
70c1503afc | ||
|
|
ee5d2eb696 | ||
|
|
2058f11931 | ||
|
|
53a9ee21d4 | ||
|
|
04118a5761 | ||
|
|
c101c30690 | ||
|
|
0e664d3471 | ||
|
|
fcb1749f10 | ||
|
|
180eb098f1 | ||
|
|
cc9b3ec296 | ||
|
|
66bd71a5d7 | ||
|
|
3365907989 | ||
|
|
6b2040f98d | ||
|
|
90551c6ce2 | ||
|
|
d3b8390df3 | ||
|
|
9554fe0bf8 | ||
|
|
5a13f31bce | ||
|
|
41bddb48a1 | ||
|
|
b820cf2e39 | ||
|
|
fd8638652d | ||
|
|
01ae5dbb2e | ||
|
|
b4797d6295 | ||
|
|
c9195a0f45 | ||
|
|
86c56c829a | ||
|
|
ef023ebad3 | ||
|
|
0c737e3de0 | ||
|
|
1b7c70eab4 | ||
|
|
6a5456f873 | ||
|
|
28b1079548 | ||
|
|
fe01d1b0df | ||
|
|
54174c308f | ||
|
|
901f764825 | ||
|
|
c1759fc384 | ||
|
|
e843b028e6 | ||
|
|
305cbecb34 | ||
|
|
a986b3fb4a | ||
|
|
fd42133d89 | ||
|
|
d6d7f3821f | ||
|
|
e79b43eeb2 | ||
|
|
afba6ce907 | ||
|
|
d5948325d4 | ||
|
|
b9b44bb87c | ||
|
|
00b00ff931 | ||
|
|
6f54a6be1e | ||
|
|
d1faccb061 | ||
|
|
0f6bb3b3ef | ||
|
|
542b87fed3 | ||
|
|
33f1df2369 | ||
|
|
041a12fdf2 | ||
|
|
555999397b | ||
|
|
25a45f6bf7 | ||
|
|
82f017edeb | ||
|
|
b4842b20f6 | ||
|
|
e14e966da9 | ||
|
|
ebe0c34a02 | ||
|
|
7d711083ec | ||
|
|
0a4d5a423e | ||
|
|
b9ccb9d31f | ||
|
|
a056b41070 | ||
|
|
6d26c44895 | ||
|
|
1644cd2da5 | ||
|
|
98135a3d30 | ||
|
|
307a41339c | ||
|
|
72ebd74715 | ||
|
|
bfa53811d3 | ||
|
|
0fa77d58c4 | ||
|
|
fa80b8ad17 | ||
|
|
9cdb6fc724 | ||
|
|
cd8e69e33c | ||
|
|
7b87f7b6b5 | ||
|
|
dd0da4643a | ||
|
|
7766526992 | ||
|
|
5c3d692008 | ||
|
|
bdf9147ad4 | ||
|
|
ad2ac256de | ||
|
|
a4466342ae | ||
|
|
0d7af592c0 | ||
|
|
66ffc5e0f8 | ||
|
|
320137a4aa | ||
|
|
0278dc9b6f | ||
|
|
7d9297e929 | ||
|
|
9e03ea8215 | ||
|
|
60f5b15f20 | ||
|
|
e67047d629 | ||
|
|
10edfa65dc | ||
|
|
daec4c70b8 | ||
|
|
dbfbc2b222 | ||
|
|
426907eafb | ||
|
|
4f0879ff9b | ||
|
|
bec6ee5c29 | ||
|
|
23fa4cec61 | ||
|
|
4b9dbd3920 | ||
|
|
98c6ced46e | ||
|
|
cb485e5af4 | ||
|
|
817b175870 | ||
|
|
bd320951e6 | ||
|
|
0cfd873abe | ||
|
|
d30ad3f5ce | ||
|
|
5dbf4fc0c2 | ||
|
|
e3a945a0b5 | ||
|
|
a5c075c4e2 | ||
|
|
c0dd7c5975 | ||
|
|
4031588811 | ||
|
|
1dc2a45cb2 | ||
|
|
40b172ca5a | ||
|
|
94031f9cef | ||
|
|
a6783cd6f3 | ||
|
|
438d85b5ad | ||
|
|
90b6ccd321 | ||
|
|
db778fd456 | ||
|
|
08f3a0791d | ||
|
|
663f824fc4 | ||
|
|
2700a94d49 | ||
|
|
e31f40c2d0 | ||
|
|
fc073cb81c | ||
|
|
2e90aaf7af | ||
|
|
238b890d9b | ||
|
|
49119e31bf | ||
|
|
bb5f1e8173 | ||
|
|
05fbd490da | ||
|
|
5322f057a0 | ||
|
|
086cc03f05 | ||
|
|
d67514b657 | ||
|
|
a467fbea0d | ||
|
|
6686c67a41 | ||
|
|
73f36fc8b7 | ||
|
|
bd8a2cc18c | ||
|
|
6d1b7e94d1 | ||
|
|
9eff939b02 | ||
|
|
0a8b27ff49 | ||
|
|
72752165df | ||
|
|
9b21d3f206 | ||
|
|
dde0a81677 | ||
|
|
31576fac61 | ||
|
|
82846777a7 | ||
|
|
7f49e0fddc | ||
|
|
eda8b02a8d | ||
|
|
1fd1617427 | ||
|
|
97252a8b66 | ||
|
|
4a1cc792c9 | ||
|
|
581b3a110c | ||
|
|
e118682db1 | ||
|
|
4eeb1c4f31 | ||
|
|
e2c4730e17 | ||
|
|
c3e844e561 | ||
|
|
fde947e1a8 | ||
|
|
ce0af892aa | ||
|
|
3f389238f8 | ||
|
|
1faf95273c | ||
|
|
ba5d4ae42f | ||
|
|
d18124f5ed | ||
|
|
846cf781a1 | ||
|
|
85dd51ccc8 | ||
|
|
ded88700a3 | ||
|
|
a9d1f40c29 | ||
|
|
e2d19aab39 | ||
|
|
7210e443ee | ||
|
|
f674c57d1a | ||
|
|
9dec27371d | ||
|
|
75328b66e6 | ||
|
|
cf9d345382 | ||
|
|
0d8392bc45 | ||
|
|
47d2d20d81 | ||
|
|
b0a5740898 | ||
|
|
bc8c4b3ebd | ||
|
|
2eebe6c677 | ||
|
|
8c326c5e66 | ||
|
|
8e9034f074 | ||
|
|
612fb96d02 | ||
|
|
49d067d72e | ||
|
|
8e1301b6d7 | ||
|
|
8ac5af2896 | ||
|
|
8550ea0728 | ||
|
|
6e619e0a70 | ||
|
|
5b2b71bfd4 | ||
|
|
d92322a574 | ||
|
|
7e793b9419 | ||
|
|
d81b703f10 | ||
|
|
1265cb9952 | ||
|
|
124e58e42d | ||
|
|
2697b63bcd | ||
|
|
ee5b836e27 | ||
|
|
a4c17dfb19 | ||
|
|
00c0d62c9b | ||
|
|
a5d4c20905 | ||
|
|
0335c6d750 | ||
|
|
8b6e42317b | ||
|
|
56e6ae567c | ||
|
|
33b663e03d | ||
|
|
4bfbe7ec22 | ||
|
|
cf37c477bb |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,7 +13,6 @@ include/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.swp
|
||||
*.html
|
||||
*.class
|
||||
*.orig
|
||||
*~
|
||||
|
||||
@@ -9,6 +9,7 @@ lib/
|
||||
bin/
|
||||
include/
|
||||
.Python/
|
||||
.env/
|
||||
|
||||
# These lines are suggested according to the svn:ignore property
|
||||
# Feel free to enable them by uncommenting them
|
||||
@@ -24,9 +25,12 @@ syntax:glob
|
||||
doc/*/_build
|
||||
build/
|
||||
dist/
|
||||
testing/cx_freeze/build
|
||||
testing/cx_freeze/cx_freeze_source
|
||||
*.egg-info
|
||||
issue/
|
||||
env/
|
||||
env3/
|
||||
3rdparty/
|
||||
.tox
|
||||
.cache
|
||||
|
||||
13
.hgtags
13
.hgtags
@@ -62,3 +62,16 @@ b93ac0cdae02effaa3c136a681cc45bba757fe46 1.4.14
|
||||
0000000000000000000000000000000000000000 1.4.14
|
||||
af860de70cc3f157ac34ca1d4bf557a057bff775 2.4.0
|
||||
8828c924acae0b4cad2e2cb92943d51da7cb744a 2.4.1
|
||||
8d051f89184bfa3033f5e59819dff9f32a612941 2.4.2
|
||||
a064ad64d167508a8e9e73766b1a4e6bd10c85db 2.5.0
|
||||
039d543d1ca02a716c0b0de9a7131beb8021e8a2 2.5.1
|
||||
421d3b4d150d901de24b1cbeb8955547b1420483 2.5.2
|
||||
60725b17a9d1af4100abb8be3f9f4ddf6262bf34 2.6.0
|
||||
60725b17a9d1af4100abb8be3f9f4ddf6262bf34 2.6.0
|
||||
88af949b9611494e2c65d528f9e565b00fb7e8ca 2.6.0
|
||||
a4f9639702baa3eb4f3b16e162f74f7b69f3f9e1 2.6.1
|
||||
a4f25c5e649892b5cc746d21be971e4773478af9 2.6.2
|
||||
2967aa416a4f3cdb65fc75073a2a148e1f372742 2.6.3
|
||||
f03b6de8325f5b6c35cea7c3de092f134ea8ef07 2.6.4
|
||||
7ed701fa2fb554bfc0618d447dfec700cc697407 2.7.0
|
||||
edc1d080bab5a970da8f6c776be50768829a7b09 2.7.1
|
||||
|
||||
29
.travis.yml
29
.travis.yml
@@ -1,8 +1,33 @@
|
||||
sudo: false
|
||||
language: python
|
||||
# command to install dependencies
|
||||
install: "pip install -U detox"
|
||||
install: "pip install -U tox"
|
||||
# # command to run tests
|
||||
script: detox --recreate
|
||||
env:
|
||||
matrix:
|
||||
- TESTENV=flakes
|
||||
- TESTENV=py26
|
||||
- TESTENV=py27
|
||||
- TESTENV=py34
|
||||
- TESTENV=pypy
|
||||
- TESTENV=py27-pexpect
|
||||
- TESTENV=py33-pexpect
|
||||
- TESTENV=py27-nobyte
|
||||
- TESTENV=py33
|
||||
- TESTENV=py27-xdist
|
||||
- TESTENV=py33-xdist
|
||||
- TESTENV=py27
|
||||
- TESTENV=py27-trial
|
||||
- TESTENV=py33
|
||||
- TESTENV=py33-trial
|
||||
# inprocess tests by default were introduced in 2.8 only;
|
||||
# this TESTENV should be enabled when merged back to master
|
||||
#- TESTENV=py27-subprocess
|
||||
- TESTENV=doctesting
|
||||
- TESTENV=py27-cxfreeze
|
||||
- TESTENV=coveralls
|
||||
script: tox --recreate -i ALL=https://devpi.net/hpk/dev/ -e $TESTENV
|
||||
|
||||
notifications:
|
||||
irc:
|
||||
- "chat.freenode.net#pytest-dev"
|
||||
|
||||
74
AUTHORS
74
AUTHORS
@@ -1,37 +1,53 @@
|
||||
Holger Krekel, holger at merlinux eu
|
||||
merlinux GmbH, Germany, office at merlinux eu
|
||||
|
||||
Contributors include::
|
||||
Contributors include::
|
||||
|
||||
Ronny Pfannschmidt
|
||||
Anatoly Bubenkoff
|
||||
Andreas Zeidler
|
||||
Andy Freeland
|
||||
Anthon van der Neut
|
||||
Armin Rigo
|
||||
Aron Curzon
|
||||
Benjamin Peterson
|
||||
Floris Bruynooghe
|
||||
Jason R. Coombs
|
||||
Wouter van Ackooy
|
||||
Samuele Pedroni
|
||||
Anatoly Bubenkoff
|
||||
Bob Ippolito
|
||||
Brian Dorsey
|
||||
Brian Okken
|
||||
Brianna Laugher
|
||||
Carl Friedrich Bolz
|
||||
Armin Rigo
|
||||
Maho
|
||||
Jaap Broekhuizen
|
||||
Maciek Fijalkowski
|
||||
Guido Wesdorp
|
||||
Brian Dorsey
|
||||
Ross Lawley
|
||||
Ralf Schmitt
|
||||
Chris Lamb
|
||||
Harald Armin Massa
|
||||
Martijn Faassen
|
||||
Ian Bicking
|
||||
Jan Balster
|
||||
Grig Gheorghiu
|
||||
Bob Ippolito
|
||||
Christian Tismer
|
||||
Daniel Nuri
|
||||
Graham Horler
|
||||
Andreas Zeidler
|
||||
Brian Okken
|
||||
Katarzyna Jachim
|
||||
Charles Cloud
|
||||
Chris Lamb
|
||||
Christian Theunert
|
||||
Anthon van der Neut
|
||||
Christian Tismer
|
||||
Christopher Gilling
|
||||
Daniel Grana
|
||||
Daniel Nuri
|
||||
Dave Hunt
|
||||
David Mohr
|
||||
Edison Gustavo Muenz
|
||||
Floris Bruynooghe
|
||||
Graham Horler
|
||||
Grig Gheorghiu
|
||||
Guido Wesdorp
|
||||
Harald Armin Massa
|
||||
Ian Bicking
|
||||
Jaap Broekhuizen
|
||||
Jan Balster
|
||||
Jason R. Coombs
|
||||
Jurko Gospodnetić
|
||||
Katarzyna Jachim
|
||||
Maciek Fijalkowski
|
||||
Maho
|
||||
Marc Schlaich
|
||||
Mark Abramowitz
|
||||
Martijn Faassen
|
||||
Nicolas Delaby
|
||||
Piotr Banaszkiewicz
|
||||
Punyashloka Biswal
|
||||
Ralf Schmitt
|
||||
Ronny Pfannschmidt
|
||||
Ross Lawley
|
||||
Samuele Pedroni
|
||||
Tom Viner
|
||||
Trevor Bekolay
|
||||
Wouter van Ackooy
|
||||
|
||||
214
CONTRIBUTING.rst
Normal file
214
CONTRIBUTING.rst
Normal file
@@ -0,0 +1,214 @@
|
||||
============================
|
||||
Contribution getting started
|
||||
============================
|
||||
|
||||
Contributions are highly welcomed and appreciated. Every little help counts,
|
||||
so do not hesitate!
|
||||
|
||||
.. contents:: Contribution links
|
||||
:depth: 2
|
||||
|
||||
|
||||
.. _submitplugin:
|
||||
|
||||
Submit a plugin, co-develop pytest
|
||||
----------------------------------
|
||||
|
||||
Pytest development of the core, some plugins and support code happens
|
||||
in repositories living under:
|
||||
|
||||
- `the pytest-dev bitbucket team <https://bitbucket.org/pytest-dev>`_
|
||||
|
||||
- `the pytest-dev github organisation <https://github.com/pytest-dev>`_
|
||||
|
||||
All pytest-dev team members have write access to all contained
|
||||
repositories. pytest core and plugins are generally developed
|
||||
using `pull requests`_ to respective repositories.
|
||||
|
||||
You can submit your plugin by subscribing to the `pytest-dev mail list
|
||||
<https://mail.python.org/mailman/listinfo/pytest-dev>`_ and writing a
|
||||
mail pointing to your existing pytest plugin repository which must have
|
||||
the following:
|
||||
|
||||
- PyPI presence with a ``setup.py`` that contains a license, ``pytest-``
|
||||
prefixed, version number, authors, short and long description.
|
||||
|
||||
- a ``tox.ini`` for running tests using `tox <http://tox.testrun.org>`_.
|
||||
|
||||
- a ``README.txt`` describing how to use the plugin and on which
|
||||
platforms it runs.
|
||||
|
||||
- a ``LICENSE.txt`` file or equivalent containing the licensing
|
||||
information, with matching info in ``setup.py``.
|
||||
|
||||
- an issue tracker unless you rather want to use the core ``pytest``
|
||||
issue tracker.
|
||||
|
||||
If no contributor strongly objects and two agree, the repo will be
|
||||
transferred to the ``pytest-dev`` organisation and you'll become a
|
||||
member of the ``pytest-dev`` team, with commit rights to all projects.
|
||||
We recommend that each plugin has at least three people who have the
|
||||
right to release to pypi.
|
||||
|
||||
|
||||
.. _reportbugs:
|
||||
|
||||
Report bugs
|
||||
-----------
|
||||
|
||||
Report bugs for pytest at https://bitbucket.org/pytest-dev/pytest/issues
|
||||
|
||||
If you are reporting a bug, please include:
|
||||
|
||||
* Your operating system name and version.
|
||||
* Any details about your local setup that might be helpful in troubleshooting,
|
||||
specifically Python interpreter version,
|
||||
installed libraries and pytest version.
|
||||
* Detailed steps to reproduce the bug.
|
||||
|
||||
.. _submitfeedback:
|
||||
|
||||
Submit feedback for developers
|
||||
------------------------------
|
||||
|
||||
Do you like pytest? Share some love on Twitter or in your blog posts!
|
||||
|
||||
We'd also like to hear about your propositions and suggestions. Feel free to
|
||||
`submit them as issues <https://bitbucket.org/pytest-dev/pytest/issues>`__ and:
|
||||
|
||||
* Set the "kind" to "enhancement" or "proposal" so that we can quickly find
|
||||
about them.
|
||||
* Explain in detail how they should work.
|
||||
* Keep the scope as narrow as possible. This will make it easier to implement.
|
||||
* If you have required skills and/or knowledge, we are very happy for
|
||||
:ref:`pull requests <pull-requests>`.
|
||||
|
||||
.. _fixbugs:
|
||||
|
||||
Fix bugs
|
||||
--------
|
||||
|
||||
Look through the BitBucket issues for bugs. Here is sample filter you can use:
|
||||
https://bitbucket.org/pytest-dev/pytest/issues?status=new&status=open&kind=bug
|
||||
|
||||
:ref:`Talk <contact>` to developers to find out how you can fix specific bugs.
|
||||
|
||||
.. _writeplugins:
|
||||
|
||||
Implement features
|
||||
------------------
|
||||
|
||||
Look through the BitBucket issues for enhancements. Here is sample filter you
|
||||
can use:
|
||||
https://bitbucket.org/pytest-dev/pytest/issues?status=new&status=open&kind=enhancement
|
||||
|
||||
:ref:`Talk <contact>` to developers to find out how you can implement specific
|
||||
features.
|
||||
|
||||
Write documentation
|
||||
-------------------
|
||||
|
||||
pytest could always use more documentation. What exactly is needed?
|
||||
|
||||
* More complementary documentation. Have you perhaps found something unclear?
|
||||
* Documentation translations. We currently have English and Japanese versions.
|
||||
* Docstrings. There's never too much of them.
|
||||
* Blog posts, articles and such -- they're all very appreciated.
|
||||
|
||||
.. _`pull requests`:
|
||||
.. _pull-requests:
|
||||
|
||||
Preparing Pull Requests on Bitbucket
|
||||
------------------------------------
|
||||
|
||||
.. note::
|
||||
What is a "pull request"? It informs project's core developers about the
|
||||
changes you want to review and merge. Pull requests are stored on
|
||||
`BitBucket servers <https://bitbucket.org/pytest-dev/pytest/pull-requests>`__.
|
||||
Once you send pull request, we can discuss it's potential modifications and
|
||||
even add more commits to it later on.
|
||||
|
||||
The primary development platform for pytest is BitBucket. You can find all
|
||||
the issues there and submit your pull requests.
|
||||
|
||||
#. Fork the
|
||||
`pytest BitBucket repository <https://bitbucket.org/pytest-dev/pytest>`__. It's
|
||||
fine to use ``pytest`` as your fork repository name because it will live
|
||||
under your user.
|
||||
|
||||
#. Clone your fork locally using `Mercurial <http://mercurial.selenic.com/>`_
|
||||
(``hg``) and create a branch::
|
||||
|
||||
$ hg clone ssh://hg@bitbucket.org/YOUR_BITBUCKET_USERNAME/pytest
|
||||
$ cd pytest
|
||||
$ hg up pytest-2.7 # if you want to fix a bug for the pytest-2.7 series
|
||||
$ hg up default # if you want to add a feature bound for the next minor release
|
||||
$ hg branch your-branch-name # your feature/bugfix branch
|
||||
|
||||
If you need some help with Mercurial, follow this quick start
|
||||
guide: http://mercurial.selenic.com/wiki/QuickStart
|
||||
|
||||
#. Create a development environment
|
||||
(will implicitly use http://www.virtualenv.org/en/latest/)::
|
||||
|
||||
$ make develop
|
||||
$ source .env/bin/activate
|
||||
|
||||
#. You can now edit your local working copy.
|
||||
|
||||
You need to have Python 2.7 and 3.4 available in your system. Now
|
||||
running tests is as simple as issuing this command::
|
||||
|
||||
$ python runtox.py -e py27,py34,flakes
|
||||
|
||||
This command will run tests via the "tox" tool against Python 2.7 and 3.4
|
||||
and also perform "flakes" coding-style checks. ``runtox.py`` is
|
||||
a thin wrapper around ``tox`` which installs from a development package
|
||||
index where newer (not yet released to pypi) versions of dependencies
|
||||
(especially ``py``) might be present.
|
||||
|
||||
To run tests on py27 and pass options (e.g. enter pdb on failure)
|
||||
to pytest you can do::
|
||||
|
||||
$ python runtox.py -e py27 -- --pdb
|
||||
|
||||
or to only run tests in a particular test module on py34::
|
||||
|
||||
$ python runtox.py -e py34 -- testing/test_config.py
|
||||
|
||||
#. Commit and push once your tests pass and you are happy with your change(s)::
|
||||
|
||||
$ hg commit -m"<commit message>"
|
||||
$ hg push -b .
|
||||
|
||||
#. Finally, submit a pull request through the BitBucket website:
|
||||
|
||||
.. image:: img/pullrequest.png
|
||||
:width: 700px
|
||||
:align: center
|
||||
|
||||
::
|
||||
|
||||
source: YOUR_BITBUCKET_USERNAME/pytest
|
||||
branch: your-branch-name
|
||||
|
||||
target: pytest-dev/pytest
|
||||
branch: default # if it's a feature
|
||||
branch: pytest-VERSION # if it's a bugfix
|
||||
|
||||
|
||||
.. _contribution-using-git:
|
||||
|
||||
Using git with bitbucket/hg
|
||||
-------------------------------
|
||||
|
||||
There used to be the pytest GitHub mirror. It was removed in favor of the
|
||||
Mercurial one, to remove confusion of people not knowing where it's better to
|
||||
put their issues and pull requests. Also it wasn't easily possible to automate
|
||||
the mirroring process.
|
||||
|
||||
In general we recommend to work with the same version control system of the
|
||||
original repository. If you insist on using git with bitbucket/hg you
|
||||
may try `gitifyhg <https://github.com/buchuki/gitifyhg>`_ but are on your
|
||||
own and need to submit pull requests through the respective platform,
|
||||
nevertheless.
|
||||
59
HOWTORELEASE.rst
Normal file
59
HOWTORELEASE.rst
Normal file
@@ -0,0 +1,59 @@
|
||||
|
||||
How to release pytest (draft)
|
||||
--------------------------------------------
|
||||
|
||||
1. bump version numbers in _pytest/__init__.py (setup.py reads it)
|
||||
|
||||
2. check and finalize CHANGELOG
|
||||
|
||||
3. write doc/en/announce/release-VERSION.txt and include
|
||||
it in doc/en/announce/index.txt
|
||||
|
||||
4. use devpi for uploading a release tarball to a staging area:
|
||||
- ``devpi use https://devpi.net/USER/dev``
|
||||
- ``devpi upload --formats sdist,bdist_wheel``
|
||||
|
||||
5. run from multiple machines:
|
||||
- ``devpi use https://devpi.net/USER/dev``
|
||||
- ``devpi test pytest==VERSION``
|
||||
|
||||
6. check that tests pass for relevant combinations with
|
||||
``devpi list pytest``
|
||||
or look at failures with "devpi list -f pytest".
|
||||
There will be some failed environments like e.g. the py33-trial
|
||||
or py27-pexpect tox environments on Win32 platforms
|
||||
which is ok (tox does not support skipping on
|
||||
per-platform basis yet).
|
||||
|
||||
7. Regenerate the docs examples using tox::
|
||||
# Create and activate a virtualenv with regendoc installed
|
||||
# (currently needs revision 4a9ec1035734)
|
||||
tox -e regen
|
||||
|
||||
8. Build the docs, you need a virtualenv with, py and sphinx
|
||||
installed::
|
||||
cd docs/en
|
||||
make html
|
||||
|
||||
9. Tag the release::
|
||||
hg tag VERSION
|
||||
|
||||
10. Upload the docs using docs/en/Makefile::
|
||||
cd docs/en
|
||||
make install # or "installall" if you have LaTeX installed
|
||||
This requires ssh-login permission on pytest.org because it uses
|
||||
rsync.
|
||||
Note that the "install" target of doc/en/Makefile defines where the
|
||||
rsync goes to, typically to the "latest" section of pytest.org.
|
||||
|
||||
11. publish to pypi "devpi push pytest-VERSION pypi:NAME" where NAME
|
||||
is the name of pypi.python.org as configured in your
|
||||
~/.pypirc file -- it's the same you would use with
|
||||
"setup.py upload -r NAME"
|
||||
|
||||
12. send release announcement to mailing lists:
|
||||
|
||||
pytest-dev
|
||||
testing-in-python
|
||||
python-announce-list@python.org
|
||||
|
||||
59
ISSUES.txt
59
ISSUES.txt
@@ -49,7 +49,7 @@ probably makes sense in order to keep the declarative nature. This mirrors
|
||||
the marker-mechanism with respect to a test module but puts it to a directory
|
||||
scale.
|
||||
|
||||
When doing larger scoped parametrization it probably becomes neccessary
|
||||
When doing larger scoped parametrization it probably becomes necessary
|
||||
to allow parametrization to be ignored if the according parameter is not
|
||||
used (currently any parametrized argument that is not present in a function will cause a ValueError). Example:
|
||||
|
||||
@@ -77,16 +77,7 @@ this would run the test_hello() function three times with three
|
||||
different values for self.db. This could also work with unittest/nose
|
||||
style tests, i.e. it leverages existing test suites without needing
|
||||
to rewrite them. Together with the previously mentioned setup_test()
|
||||
maybe the setupfunc could be ommitted?
|
||||
|
||||
checks / deprecations for next release
|
||||
---------------------------------------------------------------
|
||||
tags: bug 2.4 core xdist
|
||||
|
||||
* check oejskit plugin compatibility
|
||||
* move pytest_nose out of pylib because it implicitely extends
|
||||
the protocol now - setup/teardown is called at module level.
|
||||
consider making calling of setup/teardown configurable
|
||||
maybe the setupfunc could be omitted?
|
||||
|
||||
optimizations
|
||||
---------------------------------------------------------------
|
||||
@@ -122,8 +113,8 @@ customize test function collection
|
||||
-------------------------------------------------------
|
||||
tags: feature
|
||||
|
||||
- introduce py.test.mark.nocollect for not considering a function for
|
||||
test collection at all. maybe also introduce a py.test.mark.test to
|
||||
- introduce pytest.mark.nocollect for not considering a function for
|
||||
test collection at all. maybe also introduce a pytest.mark.test to
|
||||
explicitely mark a function to become a tested one. Lookup JUnit ways
|
||||
of tagging tests.
|
||||
|
||||
@@ -135,18 +126,18 @@ in addition to the imperative pytest.importorskip also introduce
|
||||
a pytest.mark.importorskip so that the test count is more correct.
|
||||
|
||||
|
||||
introduce py.test.mark.platform
|
||||
introduce pytest.mark.platform
|
||||
-------------------------------------------------------
|
||||
tags: feature
|
||||
|
||||
Introduce nice-to-spell platform-skipping, examples:
|
||||
|
||||
@py.test.mark.platform("python3")
|
||||
@py.test.mark.platform("not python3")
|
||||
@py.test.mark.platform("win32 and not python3")
|
||||
@py.test.mark.platform("darwin")
|
||||
@py.test.mark.platform("not (jython and win32)")
|
||||
@py.test.mark.platform("not (jython and win32)", xfail=True)
|
||||
@pytest.mark.platform("python3")
|
||||
@pytest.mark.platform("not python3")
|
||||
@pytest.mark.platform("win32 and not python3")
|
||||
@pytest.mark.platform("darwin")
|
||||
@pytest.mark.platform("not (jython and win32)")
|
||||
@pytest.mark.platform("not (jython and win32)", xfail=True)
|
||||
|
||||
etc. Idea is to allow Python expressions which can operate
|
||||
on common spellings for operating systems and python
|
||||
@@ -181,8 +172,8 @@ tags: feature
|
||||
allow to name conftest.py files (in sub directories) that should
|
||||
be imported early, as to include command line options.
|
||||
|
||||
improve central py.test ini file
|
||||
----------------------------------
|
||||
improve central pytest ini file
|
||||
-------------------------------
|
||||
tags: feature
|
||||
|
||||
introduce more declarative configuration options:
|
||||
@@ -196,7 +187,7 @@ new documentation
|
||||
----------------------------------
|
||||
tags: feature
|
||||
|
||||
- logo py.test
|
||||
- logo pytest
|
||||
- examples for unittest or functional testing
|
||||
- resource management for functional testing
|
||||
- patterns: page object
|
||||
@@ -205,17 +196,17 @@ have imported module mismatch honour relative paths
|
||||
--------------------------------------------------------
|
||||
tags: bug
|
||||
|
||||
With 1.1.1 py.test fails at least on windows if an import
|
||||
With 1.1.1 pytest fails at least on windows if an import
|
||||
is relative and compared against an absolute conftest.py
|
||||
path. Normalize.
|
||||
|
||||
consider globals: py.test.ensuretemp and config
|
||||
consider globals: pytest.ensuretemp and config
|
||||
--------------------------------------------------------------
|
||||
tags: experimental-wish
|
||||
|
||||
consider deprecating py.test.ensuretemp and py.test.config
|
||||
to further reduce py.test globality. Also consider
|
||||
having py.test.config and ensuretemp coming from
|
||||
consider deprecating pytest.ensuretemp and pytest.config
|
||||
to further reduce pytest globality. Also consider
|
||||
having pytest.config and ensuretemp coming from
|
||||
a plugin rather than being there from the start.
|
||||
|
||||
|
||||
@@ -223,7 +214,7 @@ consider pytest_addsyspath hook
|
||||
-----------------------------------------
|
||||
tags: wish
|
||||
|
||||
py.test could call a new pytest_addsyspath() in order to systematically
|
||||
pytest could call a new pytest_addsyspath() in order to systematically
|
||||
allow manipulation of sys.path and to inhibit it via --no-addsyspath
|
||||
in order to more easily run against installed packages.
|
||||
|
||||
@@ -232,13 +223,13 @@ and pytest_configure.
|
||||
|
||||
|
||||
|
||||
deprecate global py.test.config usage
|
||||
deprecate global pytest.config usage
|
||||
----------------------------------------------------------------
|
||||
tags: feature
|
||||
|
||||
py.test.ensuretemp and py.test.config are probably the last
|
||||
pytest.ensuretemp and pytest.config are probably the last
|
||||
objects containing global state. Often using them is not
|
||||
neccessary. This is about trying to get rid of them, i.e.
|
||||
necessary. This is about trying to get rid of them, i.e.
|
||||
deprecating them and checking with PyPy's usages as well
|
||||
as others.
|
||||
|
||||
@@ -307,7 +298,7 @@ tags: feature
|
||||
|
||||
The idea is that you can e.g. import modules in a test and afterwards
|
||||
sys.modules, sys.meta_path etc would be reverted. It can go further
|
||||
then just importing however, e.g. current working direcroty, file
|
||||
then just importing however, e.g. current working directory, file
|
||||
descriptors, ...
|
||||
|
||||
This would probably be done by marking::
|
||||
@@ -366,7 +357,7 @@ a few use-cases come to mind:
|
||||
id, call = prepare_check(check)
|
||||
# bubble should only prevent exception propagation after a failure
|
||||
# the whole test should still fail
|
||||
# there might be need for a loer level api and taking custom markers into account
|
||||
# there might be need for a lower level api and taking custom markers into account
|
||||
with pytest.section(id, bubble=False):
|
||||
call()
|
||||
|
||||
|
||||
25
Makefile
Normal file
25
Makefile
Normal file
@@ -0,0 +1,25 @@
|
||||
# Set of targets useful for development/release process
|
||||
PYTHON = python2.7
|
||||
PATH := $(PWD)/.env/bin:$(PATH)
|
||||
|
||||
# prepare virtual python environment
|
||||
.env:
|
||||
virtualenv .env -p $(PYTHON)
|
||||
|
||||
# install all needed for development
|
||||
develop: .env
|
||||
pip install -e . tox -r requirements-docs.txt
|
||||
|
||||
# clean the development envrironment
|
||||
clean:
|
||||
-rm -rf .env
|
||||
|
||||
# generate documentation
|
||||
docs: develop
|
||||
find doc/en -name '*.txt' -not -path 'doc/en/_build/*' | xargs .env/bin/regendoc
|
||||
cd doc/en; make html
|
||||
|
||||
# upload documentation
|
||||
upload-docs: develop
|
||||
find doc/en -name '*.txt' -not -path 'doc/en/_build/*' | xargs .env/bin/regendoc --update
|
||||
cd doc/en; make install
|
||||
29
README.rst
29
README.rst
@@ -1,7 +1,15 @@
|
||||
.. image:: https://pypip.in/v/pytest/badge.png
|
||||
:target: https://pypi.python.org/pypi/pytest
|
||||
|
||||
Documentation: http://pytest.org/latest/
|
||||
|
||||
Changelog: http://pytest.org/latest/changelog.html
|
||||
|
||||
The ``py.test`` testing tool makes it easy to write small tests, yet
|
||||
Issues: https://bitbucket.org/pytest-dev/pytest/issues?status=open
|
||||
|
||||
CI: https://drone.io/bitbucket.org/pytest-dev/pytest
|
||||
|
||||
The ``pytest`` testing tool makes it easy to write small tests, yet
|
||||
scales to support complex functional testing. It provides
|
||||
|
||||
- `auto-discovery
|
||||
@@ -10,17 +18,15 @@ scales to support complex functional testing. It provides
|
||||
- detailed info on failing `assert statements <http://pytest.org/latest/assert.html>`_ (no need to remember ``self.assert*`` names)
|
||||
- `modular fixtures <http://pytest.org/latest/fixture.html>`_ for
|
||||
managing small or parametrized long-lived test resources.
|
||||
- multi-paradigm support: you can use ``py.test`` to run test suites based
|
||||
- multi-paradigm support: you can use ``pytest`` to run test suites based
|
||||
on `unittest <http://pytest.org/latest/unittest.html>`_ (or trial),
|
||||
`nose <http://pytest.org/latest/nose.html>`_
|
||||
- single-source compatibility to Python2.4 all the way up to Python3.3,
|
||||
PyPy-1.9 and Jython-2.5.1.
|
||||
- single-source compatibility from Python2.6 all the way up to
|
||||
Python3.4, PyPy-2.3, (jython-2.5 untested)
|
||||
|
||||
|
||||
- many `external plugins <http://pytest.org/latest/plugins.html#installing-external-plugins-searching>`_.
|
||||
|
||||
.. image:: https://secure.travis-ci.org/hpk42/pytest.png
|
||||
:target: http://travis-ci.org/hpk42/pytest
|
||||
|
||||
A simple example for a test::
|
||||
|
||||
# content of test_module.py
|
||||
@@ -36,13 +42,12 @@ For much more info, including PDF docs, see
|
||||
|
||||
and report bugs at:
|
||||
|
||||
http://bitbucket.org/hpk42/pytest/issues/
|
||||
http://bitbucket.org/pytest-dev/pytest/issues/
|
||||
|
||||
and checkout repos at:
|
||||
and checkout or fork repo at:
|
||||
|
||||
http://github.com/hpk42/pytest/ (mirror)
|
||||
http://bitbucket.org/hpk42/pytest/
|
||||
http://bitbucket.org/pytest-dev/pytest/
|
||||
|
||||
|
||||
Copyright Holger Krekel and others, 2004-2013
|
||||
Copyright Holger Krekel and others, 2004-2014
|
||||
Licensed under the MIT license.
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#
|
||||
__version__ = '2.4.2'
|
||||
__version__ = '2.7.2'
|
||||
|
||||
@@ -6,20 +6,32 @@ import sys
|
||||
from _pytest.monkeypatch import monkeypatch
|
||||
from _pytest.assertion import util
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("debugconfig")
|
||||
group.addoption('--assert', action="store", dest="assertmode",
|
||||
group.addoption('--assert',
|
||||
action="store",
|
||||
dest="assertmode",
|
||||
choices=("rewrite", "reinterp", "plain",),
|
||||
default="rewrite", metavar="MODE",
|
||||
help="""control assertion debugging tools.
|
||||
'plain' performs no assertion debugging.
|
||||
'reinterp' reinterprets assert statements after they failed to provide assertion expression information.
|
||||
'rewrite' (the default) rewrites assert statements in test modules on import
|
||||
to provide assert expression information. """)
|
||||
group.addoption('--no-assert', action="store_true", default=False,
|
||||
dest="noassert", help="DEPRECATED equivalent to --assert=plain")
|
||||
group.addoption('--nomagic', '--no-magic', action="store_true",
|
||||
default=False, help="DEPRECATED equivalent to --assert=plain")
|
||||
default="rewrite",
|
||||
metavar="MODE",
|
||||
help="""control assertion debugging tools. 'plain'
|
||||
performs no assertion debugging. 'reinterp'
|
||||
reinterprets assert statements after they failed
|
||||
to provide assertion expression information.
|
||||
'rewrite' (the default) rewrites assert
|
||||
statements in test modules on import to
|
||||
provide assert expression information. """)
|
||||
group.addoption('--no-assert',
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="noassert",
|
||||
help="DEPRECATED equivalent to --assert=plain")
|
||||
group.addoption('--nomagic', '--no-magic',
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="DEPRECATED equivalent to --assert=plain")
|
||||
|
||||
|
||||
class AssertionState:
|
||||
"""State for the assertion plugin."""
|
||||
@@ -28,41 +40,44 @@ class AssertionState:
|
||||
self.mode = mode
|
||||
self.trace = config.trace.root.get("assertion")
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
mode = config.getvalue("assertmode")
|
||||
if config.getvalue("noassert") or config.getvalue("nomagic"):
|
||||
mode = "plain"
|
||||
if mode == "rewrite":
|
||||
try:
|
||||
import ast
|
||||
import ast # noqa
|
||||
except ImportError:
|
||||
mode = "reinterp"
|
||||
else:
|
||||
# Both Jython and CPython 2.6.0 have AST bugs that make the
|
||||
# assertion rewriting hook malfunction.
|
||||
if (sys.platform.startswith('java') or
|
||||
sys.version_info[:3] == (2, 6, 0)):
|
||||
sys.version_info[:3] == (2, 6, 0)):
|
||||
mode = "reinterp"
|
||||
if mode != "plain":
|
||||
_load_modules(mode)
|
||||
m = monkeypatch()
|
||||
config._cleanup.append(m.undo)
|
||||
m.setattr(py.builtin.builtins, 'AssertionError',
|
||||
reinterpret.AssertionError)
|
||||
reinterpret.AssertionError) # noqa
|
||||
hook = None
|
||||
if mode == "rewrite":
|
||||
hook = rewrite.AssertionRewritingHook()
|
||||
hook = rewrite.AssertionRewritingHook() # noqa
|
||||
sys.meta_path.insert(0, hook)
|
||||
warn_about_missing_assertion(mode)
|
||||
config._assertstate = AssertionState(config, mode)
|
||||
config._assertstate.hook = hook
|
||||
config._assertstate.trace("configured with mode set to %r" % (mode,))
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
hook = config._assertstate.hook
|
||||
if hook is not None:
|
||||
if hook is not None and hook in sys.meta_path:
|
||||
sys.meta_path.remove(hook)
|
||||
|
||||
|
||||
def pytest_collection(session):
|
||||
# this hook is only called when test modules are collected
|
||||
# so for example not in the master process of pytest-xdist
|
||||
@@ -71,39 +86,62 @@ def pytest_collection(session):
|
||||
if hook is not None:
|
||||
hook.set_session(session)
|
||||
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
"""Setup the pytest_assertrepr_compare hook
|
||||
|
||||
The newinterpret and rewrite modules will use util._reprcompare if
|
||||
it exists to use custom reporting via the
|
||||
pytest_assertrepr_compare hook. This sets up this custom
|
||||
comparison for the test.
|
||||
"""
|
||||
def callbinrepr(op, left, right):
|
||||
"""Call the pytest_assertrepr_compare hook and prepare the result
|
||||
|
||||
This uses the first result from the hook and then ensures the
|
||||
following:
|
||||
* Overly verbose explanations are dropped unles -vv was used.
|
||||
* Embedded newlines are escaped to help util.format_explanation()
|
||||
later.
|
||||
* If the rewrite mode is used embedded %-characters are replaced
|
||||
to protect later % formatting.
|
||||
|
||||
The result can be formatted by util.format_explanation() for
|
||||
pretty printing.
|
||||
"""
|
||||
hook_result = item.ihook.pytest_assertrepr_compare(
|
||||
config=item.config, op=op, left=left, right=right)
|
||||
|
||||
for new_expl in hook_result:
|
||||
if new_expl:
|
||||
# Don't include pageloads of data unless we are very verbose (-vv)
|
||||
if len(''.join(new_expl[1:])) > 80*8 and item.config.option.verbose < 2:
|
||||
new_expl[1:] = ['Detailed information truncated, use "-vv" to see']
|
||||
res = '\n~'.join(new_expl)
|
||||
if (sum(len(p) for p in new_expl[1:]) > 80*8
|
||||
and item.config.option.verbose < 2):
|
||||
new_expl[1:] = [py.builtin._totext(
|
||||
'Detailed information truncated, use "-vv" to show')]
|
||||
new_expl = [line.replace("\n", "\\n") for line in new_expl]
|
||||
res = py.builtin._totext("\n~").join(new_expl)
|
||||
if item.config.getvalue("assertmode") == "rewrite":
|
||||
# The result will be fed back a python % formatting
|
||||
# operation, which will fail if there are extraneous
|
||||
# '%'s in the string. Escape them here.
|
||||
res = res.replace("%", "%%")
|
||||
return res
|
||||
util._reprcompare = callbinrepr
|
||||
|
||||
|
||||
def pytest_runtest_teardown(item):
|
||||
util._reprcompare = None
|
||||
|
||||
|
||||
def pytest_sessionfinish(session):
|
||||
hook = session.config._assertstate.hook
|
||||
if hook is not None:
|
||||
hook.session = None
|
||||
|
||||
|
||||
def _load_modules(mode):
|
||||
"""Lazily import assertion related code."""
|
||||
global rewrite, reinterpret
|
||||
from _pytest.assertion import reinterpret
|
||||
from _pytest.assertion import reinterpret # noqa
|
||||
if mode == "rewrite":
|
||||
from _pytest.assertion import rewrite
|
||||
from _pytest.assertion import rewrite # noqa
|
||||
|
||||
|
||||
def warn_about_missing_assertion(mode):
|
||||
try:
|
||||
@@ -118,8 +156,10 @@ def warn_about_missing_assertion(mode):
|
||||
specifically = "failing tests may report as passing"
|
||||
|
||||
sys.stderr.write("WARNING: " + specifically +
|
||||
" because assert statements are not executed "
|
||||
"by the underlying Python interpreter "
|
||||
"(are you using python -O?)\n")
|
||||
" because assert statements are not executed "
|
||||
"by the underlying Python interpreter "
|
||||
"(are you using python -O?)\n")
|
||||
|
||||
|
||||
# Expose this plugin's implementation for the pytest_assertrepr_compare hook
|
||||
pytest_assertrepr_compare = util.assertrepr_compare
|
||||
|
||||
@@ -286,7 +286,19 @@ class DebugInterpreter(ast.NodeVisitor):
|
||||
source = "__exprinfo_expr.%s" % (attr.attr,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
result = self.frame.eval(co, __exprinfo_expr=source_result)
|
||||
try:
|
||||
result = self.frame.eval(co, __exprinfo_expr=source_result)
|
||||
except AttributeError:
|
||||
# Maybe the attribute name needs to be mangled?
|
||||
if not attr.attr.startswith("__") or attr.attr.endswith("__"):
|
||||
raise
|
||||
source = "getattr(__exprinfo_expr.__class__, '__name__', '')"
|
||||
co = self._compile(source)
|
||||
class_name = self.frame.eval(co, __exprinfo_expr=source_result)
|
||||
mangled_attr = "_" + class_name + attr.attr
|
||||
source = "__exprinfo_expr.%s" % (mangled_attr,)
|
||||
co = self._compile(source)
|
||||
result = self.frame.eval(co, __exprinfo_expr=source_result)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
explanation = "%s\n{%s = %s.%s\n}" % (self.frame.repr(result),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import traceback
|
||||
import types
|
||||
import py
|
||||
import sys, inspect
|
||||
from compiler import parse, ast, pycodegen
|
||||
@@ -55,7 +57,7 @@ class View(object):
|
||||
def __getattr__(self, attr):
|
||||
# attributes not found in the normal hierarchy rooted on View
|
||||
# are looked up in the object's real class
|
||||
return getattr(self.__obj__, attr)
|
||||
return getattr(object.__getattribute__(self, '__obj__'), attr)
|
||||
|
||||
def __viewkey__(self):
|
||||
return self.__obj__.__class__
|
||||
@@ -355,7 +357,18 @@ class Getattr(Interpretable):
|
||||
expr.eval(frame)
|
||||
source = '__exprinfo_expr.%s' % self.attrname
|
||||
try:
|
||||
self.result = frame.eval(source, __exprinfo_expr=expr.result)
|
||||
try:
|
||||
self.result = frame.eval(source, __exprinfo_expr=expr.result)
|
||||
except AttributeError:
|
||||
# Maybe the attribute name needs to be mangled?
|
||||
if (not self.attrname.startswith("__") or
|
||||
self.attrname.endswith("__")):
|
||||
raise
|
||||
source = "getattr(__exprinfo_expr.__class__, '__name__', '')"
|
||||
class_name = frame.eval(source, __exprinfo_expr=expr.result)
|
||||
mangled_attr = "_" + class_name + self.attrname
|
||||
source = "__exprinfo_expr.%s" % (mangled_attr,)
|
||||
self.result = frame.eval(source, __exprinfo_expr=expr.result)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
@@ -466,7 +479,7 @@ def check(s, frame=None):
|
||||
def interpret(source, frame, should_fail=False):
|
||||
module = Interpretable(parse(source, 'exec').node)
|
||||
#print "got module", module
|
||||
if isinstance(frame, py.std.types.FrameType):
|
||||
if isinstance(frame, types.FrameType):
|
||||
frame = py.code.Frame(frame)
|
||||
try:
|
||||
module.run(frame)
|
||||
@@ -476,7 +489,6 @@ def interpret(source, frame, should_fail=False):
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
if should_fail:
|
||||
return ("(assertion failed, but when it was re-run for "
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
import sys
|
||||
import py
|
||||
from _pytest.assertion.util import BuiltinAssertionError
|
||||
u = py.builtin._totext
|
||||
|
||||
|
||||
class AssertionError(BuiltinAssertionError):
|
||||
def __init__(self, *args):
|
||||
BuiltinAssertionError.__init__(self, *args)
|
||||
if args:
|
||||
# on Python2.6 we get len(args)==2 for: assert 0, (x,y)
|
||||
# on Python2.7 and above we always get len(args) == 1
|
||||
# with args[0] being the (x,y) tuple.
|
||||
if len(args) > 1:
|
||||
toprint = args
|
||||
else:
|
||||
toprint = args[0]
|
||||
try:
|
||||
self.msg = str(args[0])
|
||||
except py.builtin._sysex:
|
||||
raise
|
||||
except:
|
||||
self.msg = "<[broken __repr__] %s at %0xd>" %(
|
||||
args[0].__class__, id(args[0]))
|
||||
self.msg = u(toprint)
|
||||
except Exception:
|
||||
self.msg = u(
|
||||
"<[broken __repr__] %s at %0xd>"
|
||||
% (toprint.__class__, id(toprint)))
|
||||
else:
|
||||
f = py.code.Frame(sys._getframe(1))
|
||||
try:
|
||||
@@ -37,10 +45,8 @@ class AssertionError(BuiltinAssertionError):
|
||||
|
||||
if sys.version_info > (3, 0):
|
||||
AssertionError.__module__ = "builtins"
|
||||
reinterpret_old = "old reinterpretation not available for py3"
|
||||
else:
|
||||
from _pytest.assertion.oldinterpret import interpret as reinterpret_old
|
||||
if sys.version_info >= (2, 6) or (sys.platform.startswith("java")):
|
||||
|
||||
if sys.version_info >= (2, 6) or sys.platform.startswith("java"):
|
||||
from _pytest.assertion.newinterpret import interpret as reinterpret
|
||||
else:
|
||||
reinterpret = reinterpret_old
|
||||
from _pytest.assertion.oldinterpret import interpret as reinterpret
|
||||
|
||||
@@ -15,7 +15,7 @@ import py
|
||||
from _pytest.assertion import util
|
||||
|
||||
|
||||
# py.test caches rewritten pycs in __pycache__.
|
||||
# pytest caches rewritten pycs in __pycache__.
|
||||
if hasattr(imp, "get_tag"):
|
||||
PYTEST_TAG = imp.get_tag() + "-PYTEST"
|
||||
else:
|
||||
@@ -41,6 +41,7 @@ class AssertionRewritingHook(object):
|
||||
def __init__(self):
|
||||
self.session = None
|
||||
self.modules = {}
|
||||
self._register_with_pkg_resources()
|
||||
|
||||
def set_session(self, session):
|
||||
self.fnpats = session.config.getini("python_files")
|
||||
@@ -55,8 +56,12 @@ class AssertionRewritingHook(object):
|
||||
names = name.rsplit(".", 1)
|
||||
lastname = names[-1]
|
||||
pth = None
|
||||
if path is not None and len(path) == 1:
|
||||
pth = path[0]
|
||||
if path is not None:
|
||||
# Starting with Python 3.3, path is a _NamespacePath(), which
|
||||
# causes problems if not converted to list.
|
||||
path = list(path)
|
||||
if len(path) == 1:
|
||||
pth = path[0]
|
||||
if pth is None:
|
||||
try:
|
||||
fd, fn, desc = imp.find_module(lastname, path)
|
||||
@@ -97,7 +102,7 @@ class AssertionRewritingHook(object):
|
||||
# the most magical part of the process: load the source, rewrite the
|
||||
# asserts, and load the rewritten source. We also cache the rewritten
|
||||
# module code in a special pyc. We must be aware of the possibility of
|
||||
# concurrent py.test processes rewriting and loading pycs. To avoid
|
||||
# concurrent pytest processes rewriting and loading pycs. To avoid
|
||||
# tricky race conditions, we maintain the following invariant: The
|
||||
# cached pyc is always a complete, valid pyc. Operations on it must be
|
||||
# atomic. POSIX's atomic rename comes in handy.
|
||||
@@ -117,7 +122,7 @@ class AssertionRewritingHook(object):
|
||||
# One of the path components was not a directory, likely
|
||||
# because we're in a zip file.
|
||||
write = False
|
||||
elif e == errno.EACCES:
|
||||
elif e in [errno.EACCES, errno.EROFS]:
|
||||
state.trace("read only directory: %r" % fn_pypath.dirname)
|
||||
write = False
|
||||
else:
|
||||
@@ -126,21 +131,27 @@ class AssertionRewritingHook(object):
|
||||
pyc = os.path.join(cache_dir, cache_name)
|
||||
# Notice that even if we're in a read-only directory, I'm going
|
||||
# to check for a cached pyc. This may not be optimal...
|
||||
co = _read_pyc(fn_pypath, pyc)
|
||||
co = _read_pyc(fn_pypath, pyc, state.trace)
|
||||
if co is None:
|
||||
state.trace("rewriting %r" % (fn,))
|
||||
co = _rewrite_test(state, fn_pypath)
|
||||
source_stat, co = _rewrite_test(state, fn_pypath)
|
||||
if co is None:
|
||||
# Probably a SyntaxError in the test.
|
||||
return None
|
||||
if write:
|
||||
_make_rewritten_pyc(state, fn_pypath, pyc, co)
|
||||
_make_rewritten_pyc(state, source_stat, pyc, co)
|
||||
else:
|
||||
state.trace("found cached rewritten pyc for %r" % (fn,))
|
||||
self.modules[name] = co, pyc
|
||||
return self
|
||||
|
||||
def load_module(self, name):
|
||||
# If there is an existing module object named 'fullname' in
|
||||
# sys.modules, the loader must use that existing module. (Otherwise,
|
||||
# the reload() builtin will not work correctly.)
|
||||
if name in sys.modules:
|
||||
return sys.modules[name]
|
||||
|
||||
co, pyc = self.modules.pop(name)
|
||||
# I wish I could just call imp.load_compiled here, but __file__ has to
|
||||
# be set properly. In Python 3.2+, this all would be handled correctly
|
||||
@@ -169,13 +180,30 @@ class AssertionRewritingHook(object):
|
||||
tp = desc[2]
|
||||
return tp == imp.PKG_DIRECTORY
|
||||
|
||||
def _write_pyc(state, co, source_path, pyc):
|
||||
@classmethod
|
||||
def _register_with_pkg_resources(cls):
|
||||
"""
|
||||
Ensure package resources can be loaded from this loader. May be called
|
||||
multiple times, as the operation is idempotent.
|
||||
"""
|
||||
try:
|
||||
import pkg_resources
|
||||
# access an attribute in case a deferred importer is present
|
||||
pkg_resources.__name__
|
||||
except ImportError:
|
||||
return
|
||||
|
||||
# Since pytest tests are always located in the file system, the
|
||||
# DefaultProvider is appropriate.
|
||||
pkg_resources.register_loader_type(cls, pkg_resources.DefaultProvider)
|
||||
|
||||
|
||||
def _write_pyc(state, co, source_stat, pyc):
|
||||
# Technically, we don't have to have the same pyc format as
|
||||
# (C)Python, since these "pycs" should never be seen by builtin
|
||||
# import. However, there's little reason deviate, and I hope
|
||||
# sometime to be able to use imp.load_compiled to load them. (See
|
||||
# the comment in load_module above.)
|
||||
mtime = int(source_path.mtime())
|
||||
try:
|
||||
fp = open(pyc, "wb")
|
||||
except IOError:
|
||||
@@ -187,7 +215,9 @@ def _write_pyc(state, co, source_path, pyc):
|
||||
return False
|
||||
try:
|
||||
fp.write(imp.get_magic())
|
||||
fp.write(struct.pack("<l", mtime))
|
||||
mtime = int(source_stat.mtime)
|
||||
size = source_stat.size & 0xFFFFFFFF
|
||||
fp.write(struct.pack("<ll", mtime, size))
|
||||
marshal.dump(co, fp)
|
||||
finally:
|
||||
fp.close()
|
||||
@@ -196,15 +226,16 @@ def _write_pyc(state, co, source_path, pyc):
|
||||
RN = "\r\n".encode("utf-8")
|
||||
N = "\n".encode("utf-8")
|
||||
|
||||
cookie_re = re.compile("coding[:=]\s*[-\w.]+")
|
||||
cookie_re = re.compile(r"^[ \t\f]*#.*coding[:=][ \t]*[-\w.]+")
|
||||
BOM_UTF8 = '\xef\xbb\xbf'
|
||||
|
||||
def _rewrite_test(state, fn):
|
||||
"""Try to read and rewrite *fn* and return the code object."""
|
||||
try:
|
||||
stat = fn.stat()
|
||||
source = fn.read("rb")
|
||||
except EnvironmentError:
|
||||
return None
|
||||
return None, None
|
||||
if ASCII_IS_DEFAULT_ENCODING:
|
||||
# ASCII is the default encoding in Python 2. Without a coding
|
||||
# declaration, Python 2 will complain about any bytes in the file
|
||||
@@ -220,17 +251,18 @@ def _rewrite_test(state, fn):
|
||||
end1 = source.find("\n")
|
||||
end2 = source.find("\n", end1 + 1)
|
||||
if (not source.startswith(BOM_UTF8) and
|
||||
(not cookie_re.match(source[0:end1]) or
|
||||
not cookie_re.match(source[end1:end2]))):
|
||||
cookie_re.match(source[0:end1]) is None and
|
||||
cookie_re.match(source[end1 + 1:end2]) is None):
|
||||
if hasattr(state, "_indecode"):
|
||||
return None # encodings imported us again, we don't rewrite
|
||||
# encodings imported us again, so don't rewrite.
|
||||
return None, None
|
||||
state._indecode = True
|
||||
try:
|
||||
try:
|
||||
source.decode("ascii")
|
||||
except UnicodeDecodeError:
|
||||
# Let it fail in real import.
|
||||
return None
|
||||
return None, None
|
||||
finally:
|
||||
del state._indecode
|
||||
# On Python versions which are not 2.7 and less than or equal to 3.1, the
|
||||
@@ -242,7 +274,7 @@ def _rewrite_test(state, fn):
|
||||
except SyntaxError:
|
||||
# Let this pop up again in the real import.
|
||||
state.trace("failed to parse: %r" % (fn,))
|
||||
return None
|
||||
return None, None
|
||||
rewrite_asserts(tree)
|
||||
try:
|
||||
co = compile(tree, fn.strpath, "exec")
|
||||
@@ -250,24 +282,24 @@ def _rewrite_test(state, fn):
|
||||
# It's possible that this error is from some bug in the
|
||||
# assertion rewriting, but I don't know of a fast way to tell.
|
||||
state.trace("failed to compile: %r" % (fn,))
|
||||
return None
|
||||
return co
|
||||
return None, None
|
||||
return stat, co
|
||||
|
||||
def _make_rewritten_pyc(state, fn, pyc, co):
|
||||
def _make_rewritten_pyc(state, source_stat, pyc, co):
|
||||
"""Try to dump rewritten code to *pyc*."""
|
||||
if sys.platform.startswith("win"):
|
||||
# Windows grants exclusive access to open files and doesn't have atomic
|
||||
# rename, so just write into the final file.
|
||||
_write_pyc(state, co, fn, pyc)
|
||||
_write_pyc(state, co, source_stat, pyc)
|
||||
else:
|
||||
# When not on windows, assume rename is atomic. Dump the code object
|
||||
# into a file specific to this process and atomically replace it.
|
||||
proc_pyc = pyc + "." + str(os.getpid())
|
||||
if _write_pyc(state, co, fn, proc_pyc):
|
||||
if _write_pyc(state, co, source_stat, proc_pyc):
|
||||
os.rename(proc_pyc, pyc)
|
||||
|
||||
def _read_pyc(source, pyc):
|
||||
"""Possibly read a py.test pyc containing rewritten code.
|
||||
def _read_pyc(source, pyc, trace=lambda x: None):
|
||||
"""Possibly read a pytest pyc containing rewritten code.
|
||||
|
||||
Return rewritten code if successful or None if not.
|
||||
"""
|
||||
@@ -275,23 +307,28 @@ def _read_pyc(source, pyc):
|
||||
fp = open(pyc, "rb")
|
||||
except IOError:
|
||||
return None
|
||||
try:
|
||||
with fp:
|
||||
try:
|
||||
mtime = int(source.mtime())
|
||||
data = fp.read(8)
|
||||
except EnvironmentError:
|
||||
size = source.size()
|
||||
data = fp.read(12)
|
||||
except EnvironmentError as e:
|
||||
trace('_read_pyc(%s): EnvironmentError %s' % (source, e))
|
||||
return None
|
||||
# Check for invalid or out of date pyc file.
|
||||
if (len(data) != 8 or data[:4] != imp.get_magic() or
|
||||
struct.unpack("<l", data[4:])[0] != mtime):
|
||||
if (len(data) != 12 or data[:4] != imp.get_magic() or
|
||||
struct.unpack("<ll", data[4:]) != (mtime, size)):
|
||||
trace('_read_pyc(%s): invalid or out of date pyc' % source)
|
||||
return None
|
||||
try:
|
||||
co = marshal.load(fp)
|
||||
except Exception as e:
|
||||
trace('_read_pyc(%s): marshal.load error %s' % (source, e))
|
||||
return None
|
||||
co = marshal.load(fp)
|
||||
if not isinstance(co, types.CodeType):
|
||||
# That's interesting....
|
||||
trace('_read_pyc(%s): not a code object' % source)
|
||||
return None
|
||||
return co
|
||||
finally:
|
||||
fp.close()
|
||||
|
||||
|
||||
def rewrite_asserts(mod):
|
||||
@@ -299,14 +336,64 @@ def rewrite_asserts(mod):
|
||||
AssertionRewriter().run(mod)
|
||||
|
||||
|
||||
_saferepr = py.io.saferepr
|
||||
from _pytest.assertion.util import format_explanation as _format_explanation
|
||||
def _saferepr(obj):
|
||||
"""Get a safe repr of an object for assertion error messages.
|
||||
|
||||
The assertion formatting (util.format_explanation()) requires
|
||||
newlines to be escaped since they are a special character for it.
|
||||
Normally assertion.util.format_explanation() does this but for a
|
||||
custom repr it is possible to contain one of the special escape
|
||||
sequences, especially '\n{' and '\n}' are likely to be present in
|
||||
JSON reprs.
|
||||
|
||||
"""
|
||||
repr = py.io.saferepr(obj)
|
||||
if py.builtin._istext(repr):
|
||||
t = py.builtin.text
|
||||
else:
|
||||
t = py.builtin.bytes
|
||||
return repr.replace(t("\n"), t("\\n"))
|
||||
|
||||
|
||||
from _pytest.assertion.util import format_explanation as _format_explanation # noqa
|
||||
|
||||
def _format_assertmsg(obj):
|
||||
"""Format the custom assertion message given.
|
||||
|
||||
For strings this simply replaces newlines with '\n~' so that
|
||||
util.format_explanation() will preserve them instead of escaping
|
||||
newlines. For other objects py.io.saferepr() is used first.
|
||||
|
||||
"""
|
||||
# reprlib appears to have a bug which means that if a string
|
||||
# contains a newline it gets escaped, however if an object has a
|
||||
# .__repr__() which contains newlines it does not get escaped.
|
||||
# However in either case we want to preserve the newline.
|
||||
if py.builtin._istext(obj) or py.builtin._isbytes(obj):
|
||||
s = obj
|
||||
is_repr = False
|
||||
else:
|
||||
s = py.io.saferepr(obj)
|
||||
is_repr = True
|
||||
if py.builtin._istext(s):
|
||||
t = py.builtin.text
|
||||
else:
|
||||
t = py.builtin.bytes
|
||||
s = s.replace(t("\n"), t("\n~")).replace(t("%"), t("%%"))
|
||||
if is_repr:
|
||||
s = s.replace(t("\\n"), t("\n~"))
|
||||
return s
|
||||
|
||||
def _should_repr_global_name(obj):
|
||||
return not hasattr(obj, "__name__") and not py.builtin.callable(obj)
|
||||
|
||||
def _format_boolop(explanations, is_or):
|
||||
return "(" + (is_or and " or " or " and ").join(explanations) + ")"
|
||||
explanation = "(" + (is_or and " or " or " and ").join(explanations) + ")"
|
||||
if py.builtin._istext(explanation):
|
||||
t = py.builtin.text
|
||||
else:
|
||||
t = py.builtin.bytes
|
||||
return explanation.replace(t('%'), t('%%'))
|
||||
|
||||
def _call_reprcompare(ops, results, expls, each_obj):
|
||||
for i, res, expl in zip(range(len(ops)), results, expls):
|
||||
@@ -355,6 +442,13 @@ binop_map = {
|
||||
ast.NotIn: "not in"
|
||||
}
|
||||
|
||||
# Python 3.4+ compatibility
|
||||
if hasattr(ast, "NameConstant"):
|
||||
_NameConstant = ast.NameConstant
|
||||
else:
|
||||
def _NameConstant(c):
|
||||
return ast.Name(str(c), ast.Load())
|
||||
|
||||
|
||||
def set_location(node, lineno, col_offset):
|
||||
"""Set node location information recursively."""
|
||||
@@ -370,6 +464,56 @@ def set_location(node, lineno, col_offset):
|
||||
|
||||
|
||||
class AssertionRewriter(ast.NodeVisitor):
|
||||
"""Assertion rewriting implementation.
|
||||
|
||||
The main entrypoint is to call .run() with an ast.Module instance,
|
||||
this will then find all the assert statements and re-write them to
|
||||
provide intermediate values and a detailed assertion error. See
|
||||
http://pybites.blogspot.be/2011/07/behind-scenes-of-pytests-new-assertion.html
|
||||
for an overview of how this works.
|
||||
|
||||
The entry point here is .run() which will iterate over all the
|
||||
statements in an ast.Module and for each ast.Assert statement it
|
||||
finds call .visit() with it. Then .visit_Assert() takes over and
|
||||
is responsible for creating new ast statements to replace the
|
||||
original assert statement: it re-writes the test of an assertion
|
||||
to provide intermediate values and replace it with an if statement
|
||||
which raises an assertion error with a detailed explanation in
|
||||
case the expression is false.
|
||||
|
||||
For this .visit_Assert() uses the visitor pattern to visit all the
|
||||
AST nodes of the ast.Assert.test field, each visit call returning
|
||||
an AST node and the corresponding explanation string. During this
|
||||
state is kept in several instance attributes:
|
||||
|
||||
:statements: All the AST statements which will replace the assert
|
||||
statement.
|
||||
|
||||
:variables: This is populated by .variable() with each variable
|
||||
used by the statements so that they can all be set to None at
|
||||
the end of the statements.
|
||||
|
||||
:variable_counter: Counter to create new unique variables needed
|
||||
by statements. Variables are created using .variable() and
|
||||
have the form of "@py_assert0".
|
||||
|
||||
:on_failure: The AST statements which will be executed if the
|
||||
assertion test fails. This is the code which will construct
|
||||
the failure message and raises the AssertionError.
|
||||
|
||||
:explanation_specifiers: A dict filled by .explanation_param()
|
||||
with %-formatting placeholders and their corresponding
|
||||
expressions to use in the building of an assertion message.
|
||||
This is used by .pop_format_context() to build a message.
|
||||
|
||||
:stack: A stack of the explanation_specifiers dicts maintained by
|
||||
.push_format_context() and .pop_format_context() which allows
|
||||
to build another %-formatted string while already building one.
|
||||
|
||||
This state is reset on every new assert statement visited and used
|
||||
by the other visitors.
|
||||
|
||||
"""
|
||||
|
||||
def run(self, mod):
|
||||
"""Find all assert statements in *mod* and rewrite them."""
|
||||
@@ -451,15 +595,41 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
return ast.Attribute(builtin_name, name, ast.Load())
|
||||
|
||||
def explanation_param(self, expr):
|
||||
"""Return a new named %-formatting placeholder for expr.
|
||||
|
||||
This creates a %-formatting placeholder for expr in the
|
||||
current formatting context, e.g. ``%(py0)s``. The placeholder
|
||||
and expr are placed in the current format context so that it
|
||||
can be used on the next call to .pop_format_context().
|
||||
|
||||
"""
|
||||
specifier = "py" + str(next(self.variable_counter))
|
||||
self.explanation_specifiers[specifier] = expr
|
||||
return "%(" + specifier + ")s"
|
||||
|
||||
def push_format_context(self):
|
||||
"""Create a new formatting context.
|
||||
|
||||
The format context is used for when an explanation wants to
|
||||
have a variable value formatted in the assertion message. In
|
||||
this case the value required can be added using
|
||||
.explanation_param(). Finally .pop_format_context() is used
|
||||
to format a string of %-formatted values as added by
|
||||
.explanation_param().
|
||||
|
||||
"""
|
||||
self.explanation_specifiers = {}
|
||||
self.stack.append(self.explanation_specifiers)
|
||||
|
||||
def pop_format_context(self, expl_expr):
|
||||
"""Format the %-formatted string with current format context.
|
||||
|
||||
The expl_expr should be an ast.Str instance constructed from
|
||||
the %-placeholders created by .explanation_param(). This will
|
||||
add the required code to format said string to .on_failure and
|
||||
return the ast.Name instance of the formatted string.
|
||||
|
||||
"""
|
||||
current = self.stack.pop()
|
||||
if self.stack:
|
||||
self.explanation_specifiers = self.stack[-1]
|
||||
@@ -477,11 +647,15 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
return res, self.explanation_param(self.display(res))
|
||||
|
||||
def visit_Assert(self, assert_):
|
||||
if assert_.msg:
|
||||
# There's already a message. Don't mess with it.
|
||||
return [assert_]
|
||||
"""Return the AST statements to replace the ast.Assert instance.
|
||||
|
||||
This re-writes the test of an assertion to provide
|
||||
intermediate values and replace it with an if statement which
|
||||
raises an assertion error with a detailed explanation in case
|
||||
the expression is false.
|
||||
|
||||
"""
|
||||
self.statements = []
|
||||
self.cond_chain = ()
|
||||
self.variables = []
|
||||
self.variable_counter = itertools.count()
|
||||
self.stack = []
|
||||
@@ -493,8 +667,13 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
body = self.on_failure
|
||||
negation = ast.UnaryOp(ast.Not(), top_condition)
|
||||
self.statements.append(ast.If(negation, body, []))
|
||||
explanation = "assert " + explanation
|
||||
template = ast.Str(explanation)
|
||||
if assert_.msg:
|
||||
assertmsg = self.helper('format_assertmsg', assert_.msg)
|
||||
explanation = "\n>assert " + explanation
|
||||
else:
|
||||
assertmsg = ast.Str("")
|
||||
explanation = "assert " + explanation
|
||||
template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation))
|
||||
msg = self.pop_format_context(template)
|
||||
fmt = self.helper("format_explanation", msg)
|
||||
err_name = ast.Name("AssertionError", ast.Load())
|
||||
@@ -508,7 +687,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
if self.variables:
|
||||
variables = [ast.Name(name, ast.Store())
|
||||
for name in self.variables]
|
||||
clear = ast.Assign(variables, ast.Name("None", ast.Load()))
|
||||
clear = ast.Assign(variables, _NameConstant(None))
|
||||
self.statements.append(clear)
|
||||
# Fix line numbers.
|
||||
for stmt in self.statements:
|
||||
@@ -538,7 +717,8 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
for i, v in enumerate(boolop.values):
|
||||
if i:
|
||||
fail_inner = []
|
||||
self.on_failure.append(ast.If(cond, fail_inner, []))
|
||||
# cond is set in a prior loop iteration below
|
||||
self.on_failure.append(ast.If(cond, fail_inner, [])) # noqa
|
||||
self.on_failure = fail_inner
|
||||
self.push_format_context()
|
||||
res, expl = self.visit(v)
|
||||
@@ -631,7 +811,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
res_expr = ast.Compare(left_res, [op], [next_res])
|
||||
self.statements.append(ast.Assign([store_names[i]], res_expr))
|
||||
left_res, left_expl = next_res, next_expl
|
||||
# Use py.code._reprcompare if that's available.
|
||||
# Use pytest.assertion.util._reprcompare if that's available.
|
||||
expl_call = self.helper("call_reprcompare",
|
||||
ast.Tuple(syms, ast.Load()),
|
||||
ast.Tuple(load_names, ast.Load()),
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
"""Utilities for assertion debugging"""
|
||||
import pprint
|
||||
|
||||
import py
|
||||
try:
|
||||
from collections.abc import Sequence
|
||||
from collections import Sequence
|
||||
except ImportError:
|
||||
try:
|
||||
from collections import Sequence
|
||||
except ImportError:
|
||||
Sequence = list
|
||||
|
||||
Sequence = list
|
||||
|
||||
BuiltinAssertionError = py.builtin.builtins.AssertionError
|
||||
u = py.builtin._totext
|
||||
|
||||
# The _reprcompare attribute on the util module is used by the new assertion
|
||||
# interpretation code and assertion rewriter to detect this plugin was
|
||||
@@ -29,20 +27,33 @@ def format_explanation(explanation):
|
||||
for when one explanation needs to span multiple lines, e.g. when
|
||||
displaying diffs.
|
||||
"""
|
||||
# simplify 'assert False where False = ...'
|
||||
explanation = _collapse_false(explanation)
|
||||
lines = _split_explanation(explanation)
|
||||
result = _format_lines(lines)
|
||||
return u('\n').join(result)
|
||||
|
||||
|
||||
def _collapse_false(explanation):
|
||||
"""Collapse expansions of False
|
||||
|
||||
So this strips out any "assert False\n{where False = ...\n}"
|
||||
blocks.
|
||||
"""
|
||||
where = 0
|
||||
while True:
|
||||
start = where = explanation.find("False\n{False = ", where)
|
||||
if where == -1:
|
||||
break
|
||||
level = 0
|
||||
prev_c = explanation[start]
|
||||
for i, c in enumerate(explanation[start:]):
|
||||
if c == "{":
|
||||
if prev_c + c == "\n{":
|
||||
level += 1
|
||||
elif c == "}":
|
||||
elif prev_c + c == "\n}":
|
||||
level -= 1
|
||||
if not level:
|
||||
break
|
||||
prev_c = c
|
||||
else:
|
||||
raise AssertionError("unbalanced braces: %r" % (explanation,))
|
||||
end = start + i
|
||||
@@ -51,38 +62,59 @@ def format_explanation(explanation):
|
||||
explanation = (explanation[:start] + explanation[start+15:end-1] +
|
||||
explanation[end+1:])
|
||||
where -= 17
|
||||
raw_lines = (explanation or '').split('\n')
|
||||
# escape newlines not followed by {, } and ~
|
||||
return explanation
|
||||
|
||||
|
||||
def _split_explanation(explanation):
|
||||
"""Return a list of individual lines in the explanation
|
||||
|
||||
This will return a list of lines split on '\n{', '\n}' and '\n~'.
|
||||
Any other newlines will be escaped and appear in the line as the
|
||||
literal '\n' characters.
|
||||
"""
|
||||
raw_lines = (explanation or u('')).split('\n')
|
||||
lines = [raw_lines[0]]
|
||||
for l in raw_lines[1:]:
|
||||
if l.startswith('{') or l.startswith('}') or l.startswith('~'):
|
||||
if l and l[0] in ['{', '}', '~', '>']:
|
||||
lines.append(l)
|
||||
else:
|
||||
lines[-1] += '\\n' + l
|
||||
return lines
|
||||
|
||||
|
||||
def _format_lines(lines):
|
||||
"""Format the individual lines
|
||||
|
||||
This will replace the '{', '}' and '~' characters of our mini
|
||||
formatting language with the proper 'where ...', 'and ...' and ' +
|
||||
...' text, taking care of indentation along the way.
|
||||
|
||||
Return a list of formatted lines.
|
||||
"""
|
||||
result = lines[:1]
|
||||
stack = [0]
|
||||
stackcnt = [0]
|
||||
for line in lines[1:]:
|
||||
if line.startswith('{'):
|
||||
if stackcnt[-1]:
|
||||
s = 'and '
|
||||
s = u('and ')
|
||||
else:
|
||||
s = 'where '
|
||||
s = u('where ')
|
||||
stack.append(len(result))
|
||||
stackcnt[-1] += 1
|
||||
stackcnt.append(0)
|
||||
result.append(' +' + ' '*(len(stack)-1) + s + line[1:])
|
||||
result.append(u(' +') + u(' ')*(len(stack)-1) + s + line[1:])
|
||||
elif line.startswith('}'):
|
||||
assert line.startswith('}')
|
||||
stack.pop()
|
||||
stackcnt.pop()
|
||||
result[stack[-1]] += line[1:]
|
||||
else:
|
||||
assert line.startswith('~')
|
||||
result.append(' '*len(stack) + line[1:])
|
||||
assert line[0] in ['~', '>']
|
||||
stack[-1] += 1
|
||||
indent = len(stack) if line.startswith('~') else len(stack) - 1
|
||||
result.append(u(' ')*indent + line[1:])
|
||||
assert len(stack) == 1
|
||||
return '\n'.join(result)
|
||||
return result
|
||||
|
||||
|
||||
# Provide basestring in python3
|
||||
@@ -97,7 +129,7 @@ def assertrepr_compare(config, op, left, right):
|
||||
width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op
|
||||
left_repr = py.io.saferepr(left, maxsize=int(width/2))
|
||||
right_repr = py.io.saferepr(right, maxsize=width-len(left_repr))
|
||||
summary = '%s %s %s' % (left_repr, op, right_repr)
|
||||
summary = u('%s %s %s') % (left_repr, op, right_repr)
|
||||
|
||||
issequence = lambda x: (isinstance(x, (list, tuple, Sequence))
|
||||
and not isinstance(x, basestring))
|
||||
@@ -105,28 +137,40 @@ def assertrepr_compare(config, op, left, right):
|
||||
isdict = lambda x: isinstance(x, dict)
|
||||
isset = lambda x: isinstance(x, (set, frozenset))
|
||||
|
||||
def isiterable(obj):
|
||||
try:
|
||||
iter(obj)
|
||||
return not istext(obj)
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
verbose = config.getoption('verbose')
|
||||
explanation = None
|
||||
try:
|
||||
if op == '==':
|
||||
if istext(left) and istext(right):
|
||||
explanation = _diff_text(left, right, verbose)
|
||||
elif issequence(left) and issequence(right):
|
||||
explanation = _compare_eq_sequence(left, right, verbose)
|
||||
elif isset(left) and isset(right):
|
||||
explanation = _compare_eq_set(left, right, verbose)
|
||||
elif isdict(left) and isdict(right):
|
||||
explanation = _compare_eq_dict(left, right, verbose)
|
||||
else:
|
||||
if issequence(left) and issequence(right):
|
||||
explanation = _compare_eq_sequence(left, right, verbose)
|
||||
elif isset(left) and isset(right):
|
||||
explanation = _compare_eq_set(left, right, verbose)
|
||||
elif isdict(left) and isdict(right):
|
||||
explanation = _compare_eq_dict(left, right, verbose)
|
||||
if isiterable(left) and isiterable(right):
|
||||
expl = _compare_eq_iterable(left, right, verbose)
|
||||
if explanation is not None:
|
||||
explanation.extend(expl)
|
||||
else:
|
||||
explanation = expl
|
||||
elif op == 'not in':
|
||||
if istext(left) and istext(right):
|
||||
explanation = _notin_text(left, right, verbose)
|
||||
except py.builtin._sysex:
|
||||
raise
|
||||
except:
|
||||
excinfo = py.code.ExceptionInfo()
|
||||
except Exception:
|
||||
explanation = [
|
||||
'(pytest_assertion plugin: representation of details failed. '
|
||||
'Probably an object has a faulty __repr__.)', str(excinfo)]
|
||||
u('(pytest_assertion plugin: representation of details failed. '
|
||||
'Probably an object has a faulty __repr__.)'),
|
||||
u(py.code.ExceptionInfo())]
|
||||
|
||||
if not explanation:
|
||||
return None
|
||||
@@ -135,12 +179,19 @@ def assertrepr_compare(config, op, left, right):
|
||||
|
||||
|
||||
def _diff_text(left, right, verbose=False):
|
||||
"""Return the explanation for the diff between text
|
||||
"""Return the explanation for the diff between text or bytes
|
||||
|
||||
Unless --verbose is used this will skip leading and trailing
|
||||
characters which are identical to keep the diff minimal.
|
||||
|
||||
If the input are bytes they will be safely converted to text.
|
||||
"""
|
||||
from difflib import ndiff
|
||||
explanation = []
|
||||
if isinstance(left, py.builtin.bytes):
|
||||
left = u(repr(left)[1:-1]).replace(r'\n', '\n')
|
||||
if isinstance(right, py.builtin.bytes):
|
||||
right = u(repr(right)[1:-1]).replace(r'\n', '\n')
|
||||
if not verbose:
|
||||
i = 0 # just in case left or right has zero length
|
||||
for i in range(min(len(left), len(right))):
|
||||
@@ -148,8 +199,8 @@ def _diff_text(left, right, verbose=False):
|
||||
break
|
||||
if i > 42:
|
||||
i -= 10 # Provide some context
|
||||
explanation = ['Skipping %s identical leading '
|
||||
'characters in diff, use -v to show' % i]
|
||||
explanation = [u('Skipping %s identical leading '
|
||||
'characters in diff, use -v to show') % i]
|
||||
left = left[i:]
|
||||
right = right[i:]
|
||||
if len(left) == len(right):
|
||||
@@ -158,13 +209,34 @@ def _diff_text(left, right, verbose=False):
|
||||
break
|
||||
if i > 42:
|
||||
i -= 10 # Provide some context
|
||||
explanation += ['Skipping %s identical trailing '
|
||||
'characters in diff, use -v to show' % i]
|
||||
explanation += [u('Skipping %s identical trailing '
|
||||
'characters in diff, use -v to show') % i]
|
||||
left = left[:-i]
|
||||
right = right[:-i]
|
||||
explanation += [line.strip('\n')
|
||||
for line in py.std.difflib.ndiff(left.splitlines(),
|
||||
right.splitlines())]
|
||||
for line in ndiff(left.splitlines(),
|
||||
right.splitlines())]
|
||||
return explanation
|
||||
|
||||
|
||||
def _compare_eq_iterable(left, right, verbose=False):
|
||||
if not verbose:
|
||||
return [u('Use -v to get the full diff')]
|
||||
# dynamic import to speedup pytest
|
||||
import difflib
|
||||
|
||||
try:
|
||||
left_formatting = pprint.pformat(left).splitlines()
|
||||
right_formatting = pprint.pformat(right).splitlines()
|
||||
explanation = [u('Full diff:')]
|
||||
except Exception:
|
||||
# hack: PrettyPrinter.pformat() in python 2 fails when formatting items that can't be sorted(), ie, calling
|
||||
# sorted() on a list would raise. See issue #718.
|
||||
# As a workaround, the full diff is generated by using the repr() string of each item of each container.
|
||||
left_formatting = sorted(repr(x) for x in left)
|
||||
right_formatting = sorted(repr(x) for x in right)
|
||||
explanation = [u('Full diff (fallback to calling repr on each item):')]
|
||||
explanation.extend(line.strip() for line in difflib.ndiff(left_formatting, right_formatting))
|
||||
return explanation
|
||||
|
||||
|
||||
@@ -172,19 +244,18 @@ def _compare_eq_sequence(left, right, verbose=False):
|
||||
explanation = []
|
||||
for i in range(min(len(left), len(right))):
|
||||
if left[i] != right[i]:
|
||||
explanation += ['At index %s diff: %r != %r' %
|
||||
(i, left[i], right[i])]
|
||||
explanation += [u('At index %s diff: %r != %r')
|
||||
% (i, left[i], right[i])]
|
||||
break
|
||||
if len(left) > len(right):
|
||||
explanation += [
|
||||
'Left contains more items, first extra item: %s' %
|
||||
py.io.saferepr(left[len(right)],)]
|
||||
explanation += [u('Left contains more items, first extra item: %s')
|
||||
% py.io.saferepr(left[len(right)],)]
|
||||
elif len(left) < len(right):
|
||||
explanation += [
|
||||
'Right contains more items, first extra item: %s' %
|
||||
u('Right contains more items, first extra item: %s') %
|
||||
py.io.saferepr(right[len(left)],)]
|
||||
return explanation # + _diff_text(py.std.pprint.pformat(left),
|
||||
# py.std.pprint.pformat(right))
|
||||
return explanation # + _diff_text(pprint.pformat(left),
|
||||
# pprint.pformat(right))
|
||||
|
||||
|
||||
def _compare_eq_set(left, right, verbose=False):
|
||||
@@ -192,11 +263,11 @@ def _compare_eq_set(left, right, verbose=False):
|
||||
diff_left = left - right
|
||||
diff_right = right - left
|
||||
if diff_left:
|
||||
explanation.append('Extra items in the left set:')
|
||||
explanation.append(u('Extra items in the left set:'))
|
||||
for item in diff_left:
|
||||
explanation.append(py.io.saferepr(item))
|
||||
if diff_right:
|
||||
explanation.append('Extra items in the right set:')
|
||||
explanation.append(u('Extra items in the right set:'))
|
||||
for item in diff_right:
|
||||
explanation.append(py.io.saferepr(item))
|
||||
return explanation
|
||||
@@ -207,26 +278,26 @@ def _compare_eq_dict(left, right, verbose=False):
|
||||
common = set(left).intersection(set(right))
|
||||
same = dict((k, left[k]) for k in common if left[k] == right[k])
|
||||
if same and not verbose:
|
||||
explanation += ['Omitting %s identical items, use -v to show' %
|
||||
explanation += [u('Omitting %s identical items, use -v to show') %
|
||||
len(same)]
|
||||
elif same:
|
||||
explanation += ['Common items:']
|
||||
explanation += py.std.pprint.pformat(same).splitlines()
|
||||
explanation += [u('Common items:')]
|
||||
explanation += pprint.pformat(same).splitlines()
|
||||
diff = set(k for k in common if left[k] != right[k])
|
||||
if diff:
|
||||
explanation += ['Differing items:']
|
||||
explanation += [u('Differing items:')]
|
||||
for k in diff:
|
||||
explanation += [py.io.saferepr({k: left[k]}) + ' != ' +
|
||||
py.io.saferepr({k: right[k]})]
|
||||
extra_left = set(left) - set(right)
|
||||
if extra_left:
|
||||
explanation.append('Left contains more items:')
|
||||
explanation.extend(py.std.pprint.pformat(
|
||||
explanation.append(u('Left contains more items:'))
|
||||
explanation.extend(pprint.pformat(
|
||||
dict((k, left[k]) for k in extra_left)).splitlines())
|
||||
extra_right = set(right) - set(left)
|
||||
if extra_right:
|
||||
explanation.append('Right contains more items:')
|
||||
explanation.extend(py.std.pprint.pformat(
|
||||
explanation.append(u('Right contains more items:'))
|
||||
explanation.extend(pprint.pformat(
|
||||
dict((k, right[k]) for k in extra_right)).splitlines())
|
||||
return explanation
|
||||
|
||||
@@ -237,14 +308,14 @@ def _notin_text(term, text, verbose=False):
|
||||
tail = text[index+len(term):]
|
||||
correct_text = head + tail
|
||||
diff = _diff_text(correct_text, text, verbose)
|
||||
newdiff = ['%s is contained here:' % py.io.saferepr(term, maxsize=42)]
|
||||
newdiff = [u('%s is contained here:') % py.io.saferepr(term, maxsize=42)]
|
||||
for line in diff:
|
||||
if line.startswith('Skipping'):
|
||||
if line.startswith(u('Skipping')):
|
||||
continue
|
||||
if line.startswith('- '):
|
||||
if line.startswith(u('- ')):
|
||||
continue
|
||||
if line.startswith('+ '):
|
||||
newdiff.append(' ' + line[2:])
|
||||
if line.startswith(u('+ ')):
|
||||
newdiff.append(u(' ') + line[2:])
|
||||
else:
|
||||
newdiff.append(line)
|
||||
return newdiff
|
||||
|
||||
@@ -1,248 +1,442 @@
|
||||
""" per-test stdout/stderr capturing mechanisms, ``capsys`` and ``capfd`` function arguments. """
|
||||
"""
|
||||
per-test stdout/stderr capturing mechanism.
|
||||
|
||||
"""
|
||||
from __future__ import with_statement
|
||||
|
||||
import pytest, py
|
||||
import sys
|
||||
import os
|
||||
from tempfile import TemporaryFile
|
||||
|
||||
import py
|
||||
import pytest
|
||||
|
||||
from py.io import TextIO
|
||||
unicode = py.builtin.text
|
||||
|
||||
patchsysdict = {0: 'stdin', 1: 'stdout', 2: 'stderr'}
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general")
|
||||
group._addoption('--capture', action="store", default=None,
|
||||
group._addoption(
|
||||
'--capture', action="store",
|
||||
default="fd" if hasattr(os, "dup") else "sys",
|
||||
metavar="method", choices=['fd', 'sys', 'no'],
|
||||
help="per-test capturing method: one of fd (default)|sys|no.")
|
||||
group._addoption('-s', action="store_const", const="no", dest="capture",
|
||||
help="per-test capturing method: one of fd|sys|no.")
|
||||
group._addoption(
|
||||
'-s', action="store_const", const="no", dest="capture",
|
||||
help="shortcut for --capture=no.")
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_load_initial_conftests(early_config, parser, args, __multicall__):
|
||||
ns = parser.parse_known_args(args)
|
||||
method = ns.capture
|
||||
if not method:
|
||||
method = "fd"
|
||||
if method == "fd" and not hasattr(os, "dup"):
|
||||
method = "sys"
|
||||
capman = CaptureManager(method)
|
||||
early_config.pluginmanager.register(capman, "capturemanager")
|
||||
|
||||
@pytest.mark.hookwrapper
|
||||
def pytest_load_initial_conftests(early_config, parser, args):
|
||||
ns = early_config.known_args_namespace
|
||||
pluginmanager = early_config.pluginmanager
|
||||
capman = CaptureManager(ns.capture)
|
||||
pluginmanager.register(capman, "capturemanager")
|
||||
|
||||
# make sure that capturemanager is properly reset at final shutdown
|
||||
def teardown():
|
||||
try:
|
||||
capman.reset_capturings()
|
||||
except ValueError:
|
||||
pass
|
||||
early_config.pluginmanager.add_shutdown(teardown)
|
||||
pluginmanager.add_shutdown(capman.reset_capturings)
|
||||
|
||||
# make sure logging does not raise exceptions at the end
|
||||
def silence_logging_at_shutdown():
|
||||
if "logging" in sys.modules:
|
||||
sys.modules["logging"].raiseExceptions = False
|
||||
early_config.pluginmanager.add_shutdown(silence_logging_at_shutdown)
|
||||
pluginmanager.add_shutdown(silence_logging_at_shutdown)
|
||||
|
||||
# finally trigger conftest loading but while capturing (issue93)
|
||||
capman.resumecapture()
|
||||
try:
|
||||
try:
|
||||
return __multicall__.execute()
|
||||
finally:
|
||||
out, err = capman.suspendcapture()
|
||||
except:
|
||||
capman.init_capturings()
|
||||
outcome = yield
|
||||
out, err = capman.suspendcapture()
|
||||
if outcome.excinfo is not None:
|
||||
sys.stdout.write(out)
|
||||
sys.stderr.write(err)
|
||||
raise
|
||||
|
||||
def addouterr(rep, outerr):
|
||||
for secname, content in zip(["out", "err"], outerr):
|
||||
if content:
|
||||
rep.sections.append(("Captured std%s" % secname, content))
|
||||
|
||||
class NoCapture:
|
||||
def startall(self):
|
||||
pass
|
||||
def resume(self):
|
||||
pass
|
||||
def reset(self):
|
||||
pass
|
||||
def suspend(self):
|
||||
return "", ""
|
||||
|
||||
class CaptureManager:
|
||||
def __init__(self, defaultmethod=None):
|
||||
self._method2capture = {}
|
||||
self._defaultmethod = defaultmethod
|
||||
|
||||
def _maketempfile(self):
|
||||
f = py.std.tempfile.TemporaryFile()
|
||||
newf = py.io.dupfile(f, encoding="UTF-8")
|
||||
f.close()
|
||||
return newf
|
||||
|
||||
def _makestringio(self):
|
||||
return py.io.TextIO()
|
||||
def __init__(self, method):
|
||||
self._method = method
|
||||
|
||||
def _getcapture(self, method):
|
||||
if method == "fd":
|
||||
return py.io.StdCaptureFD(now=False,
|
||||
out=self._maketempfile(), err=self._maketempfile()
|
||||
)
|
||||
return MultiCapture(out=True, err=True, Capture=FDCapture)
|
||||
elif method == "sys":
|
||||
return py.io.StdCapture(now=False,
|
||||
out=self._makestringio(), err=self._makestringio()
|
||||
)
|
||||
return MultiCapture(out=True, err=True, Capture=SysCapture)
|
||||
elif method == "no":
|
||||
return NoCapture()
|
||||
return MultiCapture(out=False, err=False, in_=False)
|
||||
else:
|
||||
raise ValueError("unknown capturing method: %r" % method)
|
||||
|
||||
def _getmethod(self, config, fspath):
|
||||
if config.option.capture:
|
||||
method = config.option.capture
|
||||
else:
|
||||
try:
|
||||
method = config._conftest.rget("option_capture", path=fspath)
|
||||
except KeyError:
|
||||
method = "fd"
|
||||
if method == "fd" and not hasattr(os, 'dup'): # e.g. jython
|
||||
method = "sys"
|
||||
return method
|
||||
def init_capturings(self):
|
||||
assert not hasattr(self, "_capturing")
|
||||
self._capturing = self._getcapture(self._method)
|
||||
self._capturing.start_capturing()
|
||||
|
||||
def reset_capturings(self):
|
||||
for name, cap in self._method2capture.items():
|
||||
cap.reset()
|
||||
cap = self.__dict__.pop("_capturing", None)
|
||||
if cap is not None:
|
||||
cap.pop_outerr_to_orig()
|
||||
cap.stop_capturing()
|
||||
|
||||
def resumecapture_item(self, item):
|
||||
method = self._getmethod(item.config, item.fspath)
|
||||
if not hasattr(item, 'outerr'):
|
||||
item.outerr = ('', '') # we accumulate outerr on the item
|
||||
return self.resumecapture(method)
|
||||
def resumecapture(self):
|
||||
self._capturing.resume_capturing()
|
||||
|
||||
def resumecapture(self, method=None):
|
||||
if hasattr(self, '_capturing'):
|
||||
raise ValueError("cannot resume, already capturing with %r" %
|
||||
(self._capturing,))
|
||||
if method is None:
|
||||
method = self._defaultmethod
|
||||
cap = self._method2capture.get(method)
|
||||
self._capturing = method
|
||||
if cap is None:
|
||||
self._method2capture[method] = cap = self._getcapture(method)
|
||||
cap.startall()
|
||||
else:
|
||||
cap.resume()
|
||||
|
||||
def suspendcapture(self, item=None):
|
||||
def suspendcapture(self, in_=False):
|
||||
self.deactivate_funcargs()
|
||||
if hasattr(self, '_capturing'):
|
||||
method = self._capturing
|
||||
cap = self._method2capture.get(method)
|
||||
if cap is not None:
|
||||
outerr = cap.suspend()
|
||||
del self._capturing
|
||||
if item:
|
||||
outerr = (item.outerr[0] + outerr[0],
|
||||
item.outerr[1] + outerr[1])
|
||||
cap = getattr(self, "_capturing", None)
|
||||
if cap is not None:
|
||||
outerr = cap.readouterr()
|
||||
cap.suspend_capturing(in_=in_)
|
||||
return outerr
|
||||
if hasattr(item, 'outerr'):
|
||||
return item.outerr
|
||||
return "", ""
|
||||
|
||||
def activate_funcargs(self, pyfuncitem):
|
||||
funcargs = getattr(pyfuncitem, "funcargs", None)
|
||||
if funcargs is not None:
|
||||
for name, capfuncarg in funcargs.items():
|
||||
if name in ('capsys', 'capfd'):
|
||||
assert not hasattr(self, '_capturing_funcarg')
|
||||
self._capturing_funcarg = capfuncarg
|
||||
capfuncarg._start()
|
||||
capfuncarg = pyfuncitem.__dict__.pop("_capfuncarg", None)
|
||||
if capfuncarg is not None:
|
||||
capfuncarg._start()
|
||||
self._capfuncarg = capfuncarg
|
||||
|
||||
def deactivate_funcargs(self):
|
||||
capturing_funcarg = getattr(self, '_capturing_funcarg', None)
|
||||
if capturing_funcarg:
|
||||
outerr = capturing_funcarg._finalize()
|
||||
del self._capturing_funcarg
|
||||
return outerr
|
||||
capfuncarg = self.__dict__.pop("_capfuncarg", None)
|
||||
if capfuncarg is not None:
|
||||
capfuncarg.close()
|
||||
|
||||
def pytest_make_collect_report(self, __multicall__, collector):
|
||||
method = self._getmethod(collector.config, collector.fspath)
|
||||
try:
|
||||
self.resumecapture(method)
|
||||
except ValueError:
|
||||
return # recursive collect, XXX refactor capturing
|
||||
# to allow for more lightweight recursive capturing
|
||||
try:
|
||||
rep = __multicall__.execute()
|
||||
finally:
|
||||
outerr = self.suspendcapture()
|
||||
addouterr(rep, outerr)
|
||||
return rep
|
||||
@pytest.mark.hookwrapper
|
||||
def pytest_make_collect_report(self, collector):
|
||||
if isinstance(collector, pytest.File):
|
||||
self.resumecapture()
|
||||
outcome = yield
|
||||
out, err = self.suspendcapture()
|
||||
rep = outcome.get_result()
|
||||
if out:
|
||||
rep.sections.append(("Captured stdout", out))
|
||||
if err:
|
||||
rep.sections.append(("Captured stderr", err))
|
||||
else:
|
||||
yield
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
@pytest.mark.hookwrapper
|
||||
def pytest_runtest_setup(self, item):
|
||||
self.resumecapture_item(item)
|
||||
self.resumecapture()
|
||||
yield
|
||||
self.suspendcapture_item(item, "setup")
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
@pytest.mark.hookwrapper
|
||||
def pytest_runtest_call(self, item):
|
||||
self.resumecapture_item(item)
|
||||
self.resumecapture()
|
||||
self.activate_funcargs(item)
|
||||
yield
|
||||
#self.deactivate_funcargs() called from suspendcapture()
|
||||
self.suspendcapture_item(item, "call")
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
@pytest.mark.hookwrapper
|
||||
def pytest_runtest_teardown(self, item):
|
||||
self.resumecapture_item(item)
|
||||
|
||||
def pytest_keyboard_interrupt(self, excinfo):
|
||||
if hasattr(self, '_capturing'):
|
||||
self.suspendcapture()
|
||||
self.resumecapture()
|
||||
yield
|
||||
self.suspendcapture_item(item, "teardown")
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_runtest_makereport(self, __multicall__, item, call):
|
||||
funcarg_outerr = self.deactivate_funcargs()
|
||||
rep = __multicall__.execute()
|
||||
outerr = self.suspendcapture(item)
|
||||
if funcarg_outerr is not None:
|
||||
outerr = (outerr[0] + funcarg_outerr[0],
|
||||
outerr[1] + funcarg_outerr[1])
|
||||
addouterr(rep, outerr)
|
||||
if not rep.passed or rep.when == "teardown":
|
||||
outerr = ('', '')
|
||||
item.outerr = outerr
|
||||
return rep
|
||||
def pytest_keyboard_interrupt(self, excinfo):
|
||||
self.reset_capturings()
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_internalerror(self, excinfo):
|
||||
self.reset_capturings()
|
||||
|
||||
def suspendcapture_item(self, item, when):
|
||||
out, err = self.suspendcapture()
|
||||
item.add_report_section(when, "out", out)
|
||||
item.add_report_section(when, "err", err)
|
||||
|
||||
error_capsysfderror = "cannot use capsys and capfd at the same time"
|
||||
|
||||
def pytest_funcarg__capsys(request):
|
||||
|
||||
@pytest.fixture
|
||||
def capsys(request):
|
||||
"""enables capturing of writes to sys.stdout/sys.stderr and makes
|
||||
captured output available via ``capsys.readouterr()`` method calls
|
||||
which return a ``(out, err)`` tuple.
|
||||
"""
|
||||
if "capfd" in request._funcargs:
|
||||
raise request.raiseerror(error_capsysfderror)
|
||||
return CaptureFixture(py.io.StdCapture)
|
||||
request.node._capfuncarg = c = CaptureFixture(SysCapture)
|
||||
return c
|
||||
|
||||
def pytest_funcarg__capfd(request):
|
||||
@pytest.fixture
|
||||
def capfd(request):
|
||||
"""enables capturing of writes to file descriptors 1 and 2 and makes
|
||||
captured output available via ``capsys.readouterr()`` method calls
|
||||
captured output available via ``capfd.readouterr()`` method calls
|
||||
which return a ``(out, err)`` tuple.
|
||||
"""
|
||||
if "capsys" in request._funcargs:
|
||||
request.raiseerror(error_capsysfderror)
|
||||
if not hasattr(os, 'dup'):
|
||||
pytest.skip("capfd funcarg needs os.dup")
|
||||
return CaptureFixture(py.io.StdCaptureFD)
|
||||
request.node._capfuncarg = c = CaptureFixture(FDCapture)
|
||||
return c
|
||||
|
||||
|
||||
class CaptureFixture:
|
||||
def __init__(self, captureclass):
|
||||
self.capture = captureclass(now=False)
|
||||
self.captureclass = captureclass
|
||||
|
||||
def _start(self):
|
||||
self.capture.startall()
|
||||
self._capture = MultiCapture(out=True, err=True, in_=False,
|
||||
Capture=self.captureclass)
|
||||
self._capture.start_capturing()
|
||||
|
||||
def _finalize(self):
|
||||
if hasattr(self, 'capture'):
|
||||
outerr = self._outerr = self.capture.reset()
|
||||
del self.capture
|
||||
return outerr
|
||||
def close(self):
|
||||
cap = self.__dict__.pop("_capture", None)
|
||||
if cap is not None:
|
||||
self._outerr = cap.pop_outerr_to_orig()
|
||||
cap.stop_capturing()
|
||||
|
||||
def readouterr(self):
|
||||
try:
|
||||
return self.capture.readouterr()
|
||||
return self._capture.readouterr()
|
||||
except AttributeError:
|
||||
return self._outerr
|
||||
|
||||
|
||||
def safe_text_dupfile(f, mode, default_encoding="UTF8"):
|
||||
""" return a open text file object that's a duplicate of f on the
|
||||
FD-level if possible.
|
||||
"""
|
||||
encoding = getattr(f, "encoding", None)
|
||||
try:
|
||||
fd = f.fileno()
|
||||
except Exception:
|
||||
if "b" not in getattr(f, "mode", "") and hasattr(f, "encoding"):
|
||||
# we seem to have a text stream, let's just use it
|
||||
return f
|
||||
else:
|
||||
newfd = os.dup(fd)
|
||||
if "b" not in mode:
|
||||
mode += "b"
|
||||
f = os.fdopen(newfd, mode, 0) # no buffering
|
||||
return EncodedFile(f, encoding or default_encoding)
|
||||
|
||||
|
||||
class EncodedFile(object):
|
||||
errors = "strict" # possibly needed by py3 code (issue555)
|
||||
def __init__(self, buffer, encoding):
|
||||
self.buffer = buffer
|
||||
self.encoding = encoding
|
||||
|
||||
def write(self, obj):
|
||||
if isinstance(obj, unicode):
|
||||
obj = obj.encode(self.encoding, "replace")
|
||||
self.buffer.write(obj)
|
||||
|
||||
def writelines(self, linelist):
|
||||
data = ''.join(linelist)
|
||||
self.write(data)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(object.__getattribute__(self, "buffer"), name)
|
||||
|
||||
|
||||
class MultiCapture(object):
|
||||
out = err = in_ = None
|
||||
|
||||
def __init__(self, out=True, err=True, in_=True, Capture=None):
|
||||
if in_:
|
||||
self.in_ = Capture(0)
|
||||
if out:
|
||||
self.out = Capture(1)
|
||||
if err:
|
||||
self.err = Capture(2)
|
||||
|
||||
def start_capturing(self):
|
||||
if self.in_:
|
||||
self.in_.start()
|
||||
if self.out:
|
||||
self.out.start()
|
||||
if self.err:
|
||||
self.err.start()
|
||||
|
||||
def pop_outerr_to_orig(self):
|
||||
""" pop current snapshot out/err capture and flush to orig streams. """
|
||||
out, err = self.readouterr()
|
||||
if out:
|
||||
self.out.writeorg(out)
|
||||
if err:
|
||||
self.err.writeorg(err)
|
||||
return out, err
|
||||
|
||||
def suspend_capturing(self, in_=False):
|
||||
if self.out:
|
||||
self.out.suspend()
|
||||
if self.err:
|
||||
self.err.suspend()
|
||||
if in_ and self.in_:
|
||||
self.in_.suspend()
|
||||
self._in_suspended = True
|
||||
|
||||
def resume_capturing(self):
|
||||
if self.out:
|
||||
self.out.resume()
|
||||
if self.err:
|
||||
self.err.resume()
|
||||
if hasattr(self, "_in_suspended"):
|
||||
self.in_.resume()
|
||||
del self._in_suspended
|
||||
|
||||
def stop_capturing(self):
|
||||
""" stop capturing and reset capturing streams """
|
||||
if hasattr(self, '_reset'):
|
||||
raise ValueError("was already stopped")
|
||||
self._reset = True
|
||||
if self.out:
|
||||
self.out.done()
|
||||
if self.err:
|
||||
self.err.done()
|
||||
if self.in_:
|
||||
self.in_.done()
|
||||
|
||||
def readouterr(self):
|
||||
""" return snapshot unicode value of stdout/stderr capturings. """
|
||||
return (self.out.snap() if self.out is not None else "",
|
||||
self.err.snap() if self.err is not None else "")
|
||||
|
||||
class NoCapture:
|
||||
__init__ = start = done = suspend = resume = lambda *args: None
|
||||
|
||||
class FDCapture:
|
||||
""" Capture IO to/from a given os-level filedescriptor. """
|
||||
|
||||
def __init__(self, targetfd, tmpfile=None):
|
||||
self.targetfd = targetfd
|
||||
try:
|
||||
self.targetfd_save = os.dup(self.targetfd)
|
||||
except OSError:
|
||||
self.start = lambda: None
|
||||
self.done = lambda: None
|
||||
else:
|
||||
if targetfd == 0:
|
||||
assert not tmpfile, "cannot set tmpfile with stdin"
|
||||
tmpfile = open(os.devnull, "r")
|
||||
self.syscapture = SysCapture(targetfd)
|
||||
else:
|
||||
if tmpfile is None:
|
||||
f = TemporaryFile()
|
||||
with f:
|
||||
tmpfile = safe_text_dupfile(f, mode="wb+")
|
||||
if targetfd in patchsysdict:
|
||||
self.syscapture = SysCapture(targetfd, tmpfile)
|
||||
else:
|
||||
self.syscapture = NoCapture()
|
||||
self.tmpfile = tmpfile
|
||||
self.tmpfile_fd = tmpfile.fileno()
|
||||
|
||||
def __repr__(self):
|
||||
return "<FDCapture %s oldfd=%s>" % (self.targetfd, self.targetfd_save)
|
||||
|
||||
def start(self):
|
||||
""" Start capturing on targetfd using memorized tmpfile. """
|
||||
try:
|
||||
os.fstat(self.targetfd_save)
|
||||
except (AttributeError, OSError):
|
||||
raise ValueError("saved filedescriptor not valid anymore")
|
||||
os.dup2(self.tmpfile_fd, self.targetfd)
|
||||
self.syscapture.start()
|
||||
|
||||
def snap(self):
|
||||
f = self.tmpfile
|
||||
f.seek(0)
|
||||
res = f.read()
|
||||
if res:
|
||||
enc = getattr(f, "encoding", None)
|
||||
if enc and isinstance(res, bytes):
|
||||
res = py.builtin._totext(res, enc, "replace")
|
||||
f.truncate(0)
|
||||
f.seek(0)
|
||||
return res
|
||||
return ''
|
||||
|
||||
def done(self):
|
||||
""" stop capturing, restore streams, return original capture file,
|
||||
seeked to position zero. """
|
||||
targetfd_save = self.__dict__.pop("targetfd_save")
|
||||
os.dup2(targetfd_save, self.targetfd)
|
||||
os.close(targetfd_save)
|
||||
self.syscapture.done()
|
||||
self.tmpfile.close()
|
||||
|
||||
def suspend(self):
|
||||
self.syscapture.suspend()
|
||||
os.dup2(self.targetfd_save, self.targetfd)
|
||||
|
||||
def resume(self):
|
||||
self.syscapture.resume()
|
||||
os.dup2(self.tmpfile_fd, self.targetfd)
|
||||
|
||||
def writeorg(self, data):
|
||||
""" write to original file descriptor. """
|
||||
if py.builtin._istext(data):
|
||||
data = data.encode("utf8") # XXX use encoding of original stream
|
||||
os.write(self.targetfd_save, data)
|
||||
|
||||
|
||||
class SysCapture:
|
||||
def __init__(self, fd, tmpfile=None):
|
||||
name = patchsysdict[fd]
|
||||
self._old = getattr(sys, name)
|
||||
self.name = name
|
||||
if tmpfile is None:
|
||||
if name == "stdin":
|
||||
tmpfile = DontReadFromInput()
|
||||
else:
|
||||
tmpfile = TextIO()
|
||||
self.tmpfile = tmpfile
|
||||
|
||||
def start(self):
|
||||
setattr(sys, self.name, self.tmpfile)
|
||||
|
||||
def snap(self):
|
||||
f = self.tmpfile
|
||||
res = f.getvalue()
|
||||
f.truncate(0)
|
||||
f.seek(0)
|
||||
return res
|
||||
|
||||
def done(self):
|
||||
setattr(sys, self.name, self._old)
|
||||
del self._old
|
||||
self.tmpfile.close()
|
||||
|
||||
def suspend(self):
|
||||
setattr(sys, self.name, self._old)
|
||||
|
||||
def resume(self):
|
||||
setattr(sys, self.name, self.tmpfile)
|
||||
|
||||
def writeorg(self, data):
|
||||
self._old.write(data)
|
||||
self._old.flush()
|
||||
|
||||
|
||||
class DontReadFromInput:
|
||||
"""Temporary stub class. Ideally when stdin is accessed, the
|
||||
capturing should be turned off, with possibly all data captured
|
||||
so far sent to the screen. This should be configurable, though,
|
||||
because in automated test runs it is better to crash than
|
||||
hang indefinitely.
|
||||
"""
|
||||
|
||||
encoding = None
|
||||
|
||||
def read(self, *args):
|
||||
raise IOError("reading from stdin while output is captured")
|
||||
readline = read
|
||||
readlines = read
|
||||
__iter__ = read
|
||||
|
||||
def fileno(self):
|
||||
raise ValueError("redirected Stdin is pseudofile, has no fileno()")
|
||||
|
||||
def isatty(self):
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
self._finalize()
|
||||
pass
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
""" command line options, ini-file and conftest.py processing. """
|
||||
import argparse
|
||||
import shlex
|
||||
import traceback
|
||||
import types
|
||||
import warnings
|
||||
|
||||
import py
|
||||
# DON't import pytest here because it causes import cycle troubles
|
||||
import sys, os
|
||||
from _pytest import hookspec # the extension point definitions
|
||||
from _pytest.core import PluginManager
|
||||
|
||||
# pytest startup
|
||||
#
|
||||
class ConftestImportFailure(Exception):
|
||||
def __init__(self, path, excinfo):
|
||||
Exception.__init__(self, path, excinfo)
|
||||
self.path = path
|
||||
self.excinfo = excinfo
|
||||
|
||||
|
||||
def main(args=None, plugins=None):
|
||||
""" return exit code, after performing an in-process test run.
|
||||
@@ -15,15 +28,23 @@ def main(args=None, plugins=None):
|
||||
:arg plugins: list of plugin objects to be auto-registered during
|
||||
initialization.
|
||||
"""
|
||||
config = _prepareconfig(args, plugins)
|
||||
exitstatus = config.hook.pytest_cmdline_main(config=config)
|
||||
return exitstatus
|
||||
try:
|
||||
config = _prepareconfig(args, plugins)
|
||||
except ConftestImportFailure:
|
||||
e = sys.exc_info()[1]
|
||||
tw = py.io.TerminalWriter(sys.stderr)
|
||||
for line in traceback.format_exception(*e.excinfo):
|
||||
tw.line(line.rstrip(), red=True)
|
||||
tw.line("ERROR: could not load %s\n" % (e.path), red=True)
|
||||
return 4
|
||||
else:
|
||||
return config.hook.pytest_cmdline_main(config=config)
|
||||
|
||||
class cmdline: # compatibility namespace
|
||||
main = staticmethod(main)
|
||||
|
||||
class UsageError(Exception):
|
||||
""" error in py.test usage or invocation"""
|
||||
""" error in pytest usage or invocation"""
|
||||
|
||||
_preinit = []
|
||||
|
||||
@@ -41,7 +62,7 @@ def get_plugin_manager():
|
||||
return _preinit.pop(0)
|
||||
# subsequent calls to main will create a fresh instance
|
||||
pluginmanager = PytestPluginManager()
|
||||
pluginmanager.config = config = Config(pluginmanager) # XXX attr needed?
|
||||
pluginmanager.config = Config(pluginmanager) # XXX attr needed?
|
||||
for spec in default_plugins:
|
||||
pluginmanager.import_plugin(spec)
|
||||
return pluginmanager
|
||||
@@ -54,13 +75,17 @@ def _prepareconfig(args=None, plugins=None):
|
||||
elif not isinstance(args, (tuple, list)):
|
||||
if not isinstance(args, str):
|
||||
raise ValueError("not a string or argument list: %r" % (args,))
|
||||
args = py.std.shlex.split(args)
|
||||
args = shlex.split(args)
|
||||
pluginmanager = get_plugin_manager()
|
||||
if plugins:
|
||||
for plugin in plugins:
|
||||
pluginmanager.register(plugin)
|
||||
return pluginmanager.hook.pytest_cmdline_parse(
|
||||
pluginmanager=pluginmanager, args=args)
|
||||
try:
|
||||
if plugins:
|
||||
for plugin in plugins:
|
||||
pluginmanager.register(plugin)
|
||||
return pluginmanager.hook.pytest_cmdline_parse(
|
||||
pluginmanager=pluginmanager, args=args)
|
||||
except Exception:
|
||||
pluginmanager.ensure_shutdown()
|
||||
raise
|
||||
|
||||
class PytestPluginManager(PluginManager):
|
||||
def __init__(self, hookspecs=[hookspec]):
|
||||
@@ -73,7 +98,7 @@ class PytestPluginManager(PluginManager):
|
||||
err = py.io.dupfile(err, encoding=encoding)
|
||||
except Exception:
|
||||
pass
|
||||
self.trace.root.setwriter(err.write)
|
||||
self.set_tracing(err.write)
|
||||
|
||||
def pytest_configure(self, config):
|
||||
config.addinivalue_line("markers",
|
||||
@@ -82,6 +107,8 @@ class PytestPluginManager(PluginManager):
|
||||
config.addinivalue_line("markers",
|
||||
"trylast: mark a hook implementation function such that the "
|
||||
"plugin machinery will try to call it last/as late as possible.")
|
||||
for warning in self._warnings:
|
||||
config.warn(code="I1", message=warning)
|
||||
|
||||
|
||||
class Parser:
|
||||
@@ -94,7 +121,6 @@ class Parser:
|
||||
self._usage = usage
|
||||
self._inidict = {}
|
||||
self._ininames = []
|
||||
self.hints = []
|
||||
|
||||
def processoption(self, option):
|
||||
if self._processopt:
|
||||
@@ -129,8 +155,8 @@ class Parser:
|
||||
|
||||
:opts: option names, can be short or long options.
|
||||
:attrs: same attributes which the ``add_option()`` function of the
|
||||
`optparse library
|
||||
<http://docs.python.org/library/optparse.html#module-optparse>`_
|
||||
`argparse library
|
||||
<http://docs.python.org/2/library/argparse.html>`_
|
||||
accepts.
|
||||
|
||||
After command line parsing options are available on the pytest config
|
||||
@@ -159,8 +185,7 @@ class Parser:
|
||||
a = option.attrs()
|
||||
arggroup.add_argument(*n, **a)
|
||||
# bash like autocompletion for dirs (appending '/')
|
||||
optparser.add_argument(FILE_OR_DIR, nargs='*'
|
||||
).completer=filescompleter
|
||||
optparser.add_argument(FILE_OR_DIR, nargs='*').completer=filescompleter
|
||||
return optparser
|
||||
|
||||
def parse_setoption(self, args, option):
|
||||
@@ -207,7 +232,7 @@ class ArgumentError(Exception):
|
||||
|
||||
|
||||
class Argument:
|
||||
"""class that mimics the necessary behaviour of py.std.optparse.Option """
|
||||
"""class that mimics the necessary behaviour of optparse.Option """
|
||||
_typ_map = {
|
||||
'int': int,
|
||||
'string': str,
|
||||
@@ -225,8 +250,8 @@ class Argument:
|
||||
try:
|
||||
help = attrs['help']
|
||||
if '%default' in help:
|
||||
py.std.warnings.warn(
|
||||
'py.test now uses argparse. "%default" should be'
|
||||
warnings.warn(
|
||||
'pytest now uses argparse. "%default" should be'
|
||||
' changed to "%(default)s" ',
|
||||
FutureWarning,
|
||||
stacklevel=3)
|
||||
@@ -241,7 +266,7 @@ class Argument:
|
||||
if isinstance(typ, py.builtin._basestring):
|
||||
if typ == 'choice':
|
||||
if self.TYPE_WARN:
|
||||
py.std.warnings.warn(
|
||||
warnings.warn(
|
||||
'type argument to addoption() is a string %r.'
|
||||
' For parsearg this is optional and when supplied '
|
||||
' should be a type.'
|
||||
@@ -253,7 +278,7 @@ class Argument:
|
||||
attrs['type'] = type(attrs['choices'][0])
|
||||
else:
|
||||
if self.TYPE_WARN:
|
||||
py.std.warnings.warn(
|
||||
warnings.warn(
|
||||
'type argument to addoption() is a string %r.'
|
||||
' For parsearg this should be a type.'
|
||||
' (options: %s)' % (typ, names),
|
||||
@@ -373,32 +398,24 @@ class OptionGroup:
|
||||
self.options.append(option)
|
||||
|
||||
|
||||
class MyOptionParser(py.std.argparse.ArgumentParser):
|
||||
class MyOptionParser(argparse.ArgumentParser):
|
||||
def __init__(self, parser):
|
||||
self._parser = parser
|
||||
py.std.argparse.ArgumentParser.__init__(self, usage=parser._usage,
|
||||
argparse.ArgumentParser.__init__(self, usage=parser._usage,
|
||||
add_help=False, formatter_class=DropShorterLongHelpFormatter)
|
||||
|
||||
def format_epilog(self, formatter):
|
||||
hints = self._parser.hints
|
||||
if hints:
|
||||
s = "\n".join(["hint: " + x for x in hints]) + "\n"
|
||||
s = "\n" + s + "\n"
|
||||
return s
|
||||
return ""
|
||||
|
||||
def parse_args(self, args=None, namespace=None):
|
||||
"""allow splitting of positional arguments"""
|
||||
args, argv = self.parse_known_args(args, namespace)
|
||||
if argv:
|
||||
for arg in argv:
|
||||
if arg and arg[0] == '-':
|
||||
msg = py.std.argparse._('unrecognized arguments: %s')
|
||||
msg = argparse._('unrecognized arguments: %s')
|
||||
self.error(msg % ' '.join(argv))
|
||||
getattr(args, FILE_OR_DIR).extend(argv)
|
||||
return args
|
||||
|
||||
class DropShorterLongHelpFormatter(py.std.argparse.HelpFormatter):
|
||||
class DropShorterLongHelpFormatter(argparse.HelpFormatter):
|
||||
"""shorten help for long options that differ only in extra hyphens
|
||||
|
||||
- collapse **long** options that are the same except for extra hyphens
|
||||
@@ -408,7 +425,7 @@ class DropShorterLongHelpFormatter(py.std.argparse.HelpFormatter):
|
||||
- cache result on action object as this is called at least 2 times
|
||||
"""
|
||||
def _format_action_invocation(self, action):
|
||||
orgstr = py.std.argparse.HelpFormatter._format_action_invocation(self, action)
|
||||
orgstr = argparse.HelpFormatter._format_action_invocation(self, action)
|
||||
if orgstr and orgstr[0] != '-': # only optional arguments
|
||||
return orgstr
|
||||
res = getattr(action, '_formatted_action_invocation', None)
|
||||
@@ -442,47 +459,41 @@ class DropShorterLongHelpFormatter(py.std.argparse.HelpFormatter):
|
||||
if len(option) == 2 or option[2] == ' ':
|
||||
return_list.append(option)
|
||||
if option[2:] == short_long.get(option.replace('-', '')):
|
||||
return_list.append(option)
|
||||
return_list.append(option.replace(' ', '='))
|
||||
action._formatted_action_invocation = ', '.join(return_list)
|
||||
return action._formatted_action_invocation
|
||||
|
||||
|
||||
class Conftest(object):
|
||||
""" the single place for accessing values and interacting
|
||||
towards conftest modules from py.test objects.
|
||||
towards conftest modules from pytest objects.
|
||||
"""
|
||||
def __init__(self, onimport=None, confcutdir=None):
|
||||
def __init__(self, onimport=None):
|
||||
self._path2confmods = {}
|
||||
self._onimport = onimport
|
||||
self._conftestpath2mod = {}
|
||||
self._confcutdir = confcutdir
|
||||
self._confcutdir = None
|
||||
|
||||
def setinitial(self, args):
|
||||
""" try to find a first anchor path for looking up global values
|
||||
from conftests. This function is usually called _before_
|
||||
argument parsing. conftest files may add command line options
|
||||
and we thus have no completely safe way of determining
|
||||
which parts of the arguments are actually related to options
|
||||
and which are file system paths. We just try here to get
|
||||
bootstrapped ...
|
||||
def setinitial(self, namespace):
|
||||
""" load initial conftest files given a preparsed "namespace".
|
||||
As conftest files may add their own command line options
|
||||
which have arguments ('--my-opt somepath') we might get some
|
||||
false positives. All builtin and 3rd party plugins will have
|
||||
been loaded, however, so common options will not confuse our logic
|
||||
here.
|
||||
"""
|
||||
current = py.path.local()
|
||||
opt = '--confcutdir'
|
||||
for i in range(len(args)):
|
||||
opt1 = str(args[i])
|
||||
if opt1.startswith(opt):
|
||||
if opt1 == opt:
|
||||
if len(args) > i:
|
||||
p = current.join(args[i+1], abs=True)
|
||||
elif opt1.startswith(opt + "="):
|
||||
p = current.join(opt1[len(opt)+1:], abs=1)
|
||||
self._confcutdir = p
|
||||
break
|
||||
self._confcutdir = current.join(namespace.confcutdir, abs=True) \
|
||||
if namespace.confcutdir else None
|
||||
testpaths = namespace.file_or_dir
|
||||
foundanchor = False
|
||||
for arg in args:
|
||||
if hasattr(arg, 'startswith') and arg.startswith("--"):
|
||||
continue
|
||||
anchor = current.join(arg, abs=1)
|
||||
for path in testpaths:
|
||||
path = str(path)
|
||||
# remove node-id syntax
|
||||
i = path.find("::")
|
||||
if i != -1:
|
||||
path = path[:i]
|
||||
anchor = current.join(path, abs=1)
|
||||
if exists(anchor): # we found some file object
|
||||
self._try_load_conftest(anchor)
|
||||
foundanchor = True
|
||||
@@ -490,7 +501,7 @@ class Conftest(object):
|
||||
self._try_load_conftest(current)
|
||||
|
||||
def _try_load_conftest(self, anchor):
|
||||
self._path2confmods[None] = self.getconftestmodules(anchor)
|
||||
self.getconftestmodules(anchor)
|
||||
# let's also consider test* subdirs
|
||||
if anchor.check(dir=1):
|
||||
for x in anchor.listdir("test*"):
|
||||
@@ -499,28 +510,22 @@ class Conftest(object):
|
||||
|
||||
def getconftestmodules(self, path):
|
||||
try:
|
||||
clist = self._path2confmods[path]
|
||||
return self._path2confmods[path]
|
||||
except KeyError:
|
||||
if path is None:
|
||||
raise ValueError("missing default conftest.")
|
||||
clist = []
|
||||
for parent in path.parts():
|
||||
if self._confcutdir and self._confcutdir.relto(parent):
|
||||
continue
|
||||
conftestpath = parent.join("conftest.py")
|
||||
if conftestpath.check(file=1):
|
||||
clist.append(self.importconftest(conftestpath))
|
||||
mod = self.importconftest(conftestpath)
|
||||
clist.append(mod)
|
||||
self._path2confmods[path] = clist
|
||||
return clist
|
||||
return clist
|
||||
|
||||
def rget(self, name, path=None):
|
||||
mod, value = self.rget_with_confmod(name, path)
|
||||
return value
|
||||
|
||||
def rget_with_confmod(self, name, path=None):
|
||||
def rget_with_confmod(self, name, path):
|
||||
modules = self.getconftestmodules(path)
|
||||
modules.reverse()
|
||||
for mod in modules:
|
||||
for mod in reversed(modules):
|
||||
try:
|
||||
return mod, getattr(mod, name)
|
||||
except AttributeError:
|
||||
@@ -528,27 +533,27 @@ class Conftest(object):
|
||||
raise KeyError(name)
|
||||
|
||||
def importconftest(self, conftestpath):
|
||||
assert conftestpath.check(), conftestpath
|
||||
try:
|
||||
return self._conftestpath2mod[conftestpath]
|
||||
except KeyError:
|
||||
pkgpath = conftestpath.pypkgpath()
|
||||
if pkgpath is None:
|
||||
_ensure_removed_sysmodule(conftestpath.purebasename)
|
||||
self._conftestpath2mod[conftestpath] = mod = conftestpath.pyimport()
|
||||
try:
|
||||
mod = conftestpath.pyimport()
|
||||
except Exception:
|
||||
raise ConftestImportFailure(conftestpath, sys.exc_info())
|
||||
self._conftestpath2mod[conftestpath] = mod
|
||||
dirpath = conftestpath.dirpath()
|
||||
if dirpath in self._path2confmods:
|
||||
for path, mods in self._path2confmods.items():
|
||||
if path and path.relto(dirpath) or path == dirpath:
|
||||
assert mod not in mods
|
||||
mods.append(mod)
|
||||
self._postimport(mod)
|
||||
if self._onimport:
|
||||
self._onimport(mod)
|
||||
return mod
|
||||
|
||||
def _postimport(self, mod):
|
||||
if self._onimport:
|
||||
self._onimport(mod)
|
||||
return mod
|
||||
|
||||
def _ensure_removed_sysmodule(modname):
|
||||
try:
|
||||
@@ -563,6 +568,11 @@ class CmdOptions(object):
|
||||
def __repr__(self):
|
||||
return "<CmdOptions %r>" %(self.__dict__,)
|
||||
|
||||
class Notset:
|
||||
def __repr__(self):
|
||||
return "<NOTSET>"
|
||||
|
||||
notset = Notset()
|
||||
FILE_OR_DIR = 'file_or_dir'
|
||||
class Config(object):
|
||||
""" access to configuration values, pluginmanager and plugin hooks. """
|
||||
@@ -613,6 +623,14 @@ class Config(object):
|
||||
self.hook.pytest_unconfigure(config=self)
|
||||
self.pluginmanager.ensure_shutdown()
|
||||
|
||||
def warn(self, code, message):
|
||||
""" generate a warning for this test session. """
|
||||
self.hook.pytest_logwarning(code=code, message=message,
|
||||
fslocation=None, nodeid=None)
|
||||
|
||||
def get_terminal_writer(self):
|
||||
return self.pluginmanager.getplugin("terminalreporter")._tw
|
||||
|
||||
def pytest_cmdline_parse(self, pluginmanager, args):
|
||||
assert self == pluginmanager.config, (self, pluginmanager.config)
|
||||
self.parse(args)
|
||||
@@ -639,6 +657,12 @@ class Config(object):
|
||||
sys.stderr.write("INTERNALERROR> %s\n" %line)
|
||||
sys.stderr.flush()
|
||||
|
||||
def cwd_relative_nodeid(self, nodeid):
|
||||
# nodeid's are relative to the rootpath, compute relative to cwd
|
||||
if self.invocation_dir != self.rootdir:
|
||||
fullpath = self.rootdir.join(nodeid)
|
||||
nodeid = self.invocation_dir.bestrelpath(fullpath)
|
||||
return nodeid
|
||||
|
||||
@classmethod
|
||||
def fromdictargs(cls, option_dict, args):
|
||||
@@ -664,31 +688,43 @@ class Config(object):
|
||||
setattr(self.option, opt.dest, opt.default)
|
||||
|
||||
def _getmatchingplugins(self, fspath):
|
||||
allconftests = self._conftest._conftestpath2mod.values()
|
||||
plugins = [x for x in self.pluginmanager.getplugins()
|
||||
if x not in allconftests]
|
||||
plugins += self._conftest.getconftestmodules(fspath)
|
||||
return plugins
|
||||
return self.pluginmanager._plugins + \
|
||||
self._conftest.getconftestmodules(fspath)
|
||||
|
||||
def pytest_load_initial_conftests(self, parser, args):
|
||||
self._conftest.setinitial(args)
|
||||
def pytest_load_initial_conftests(self, early_config):
|
||||
self._conftest.setinitial(early_config.known_args_namespace)
|
||||
pytest_load_initial_conftests.trylast = True
|
||||
|
||||
def _initini(self, args):
|
||||
self.inicfg = getcfg(args, ["pytest.ini", "tox.ini", "setup.cfg"])
|
||||
parsed_args = self._parser.parse_known_args(args)
|
||||
r = determine_setup(parsed_args.inifilename, parsed_args.file_or_dir)
|
||||
self.rootdir, self.inifile, self.inicfg = r
|
||||
self.invocation_dir = py.path.local()
|
||||
self._parser.addini('addopts', 'extra command line options', 'args')
|
||||
self._parser.addini('minversion', 'minimally required pytest version')
|
||||
|
||||
def _preparse(self, args, addopts=True):
|
||||
self._initini(args)
|
||||
if addopts:
|
||||
args[:] = shlex.split(os.environ.get('PYTEST_ADDOPTS', '')) + args
|
||||
args[:] = self.getini("addopts") + args
|
||||
self._checkversion()
|
||||
self.pluginmanager.consider_preparse(args)
|
||||
self.pluginmanager.consider_setuptools_entrypoints()
|
||||
self.pluginmanager.consider_env()
|
||||
self.hook.pytest_load_initial_conftests(early_config=self,
|
||||
args=args, parser=self._parser)
|
||||
self.known_args_namespace = ns = self._parser.parse_known_args(args)
|
||||
try:
|
||||
self.hook.pytest_load_initial_conftests(early_config=self,
|
||||
args=args, parser=self._parser)
|
||||
except ConftestImportFailure:
|
||||
e = sys.exc_info()[1]
|
||||
if ns.help or ns.version:
|
||||
# we don't want to prevent --help/--version to work
|
||||
# so just let is pass and print a warning at the end
|
||||
self.pluginmanager._warnings.append(
|
||||
"could not load initial conftests (%s)\n" % e.path)
|
||||
else:
|
||||
raise
|
||||
|
||||
def _checkversion(self):
|
||||
import pytest
|
||||
@@ -704,17 +740,15 @@ class Config(object):
|
||||
|
||||
def parse(self, args):
|
||||
# parse given cmdline arguments into this config object.
|
||||
# Note that this can only be called once per testing process.
|
||||
assert not hasattr(self, 'args'), (
|
||||
"can only parse cmdline args at most once per Config object")
|
||||
self._origargs = args
|
||||
self._preparse(args)
|
||||
# XXX deprecated hook:
|
||||
self.hook.pytest_cmdline_preparse(config=self, args=args)
|
||||
self._parser.hints.extend(self.pluginmanager._hints)
|
||||
args = self._parser.parse_setoption(args, self.option)
|
||||
if not args:
|
||||
args.append(py.std.os.getcwd())
|
||||
args.append(os.getcwd())
|
||||
self.args = args
|
||||
|
||||
def addinivalue_line(self, name, line):
|
||||
@@ -752,18 +786,18 @@ class Config(object):
|
||||
if type == "pathlist":
|
||||
dp = py.path.local(self.inicfg.config.path).dirpath()
|
||||
l = []
|
||||
for relpath in py.std.shlex.split(value):
|
||||
for relpath in shlex.split(value):
|
||||
l.append(dp.join(relpath, abs=True))
|
||||
return l
|
||||
elif type == "args":
|
||||
return py.std.shlex.split(value)
|
||||
return shlex.split(value)
|
||||
elif type == "linelist":
|
||||
return [t for t in map(lambda x: x.strip(), value.split("\n")) if t]
|
||||
else:
|
||||
assert type is None
|
||||
return value
|
||||
|
||||
def _getconftest_pathlist(self, name, path=None):
|
||||
def _getconftest_pathlist(self, name, path):
|
||||
try:
|
||||
mod, relroots = self._conftest.rget_with_confmod(name, path)
|
||||
except KeyError:
|
||||
@@ -777,47 +811,36 @@ class Config(object):
|
||||
l.append(relroot)
|
||||
return l
|
||||
|
||||
def _getconftest(self, name, path=None, check=False):
|
||||
if check:
|
||||
self._checkconftest(name)
|
||||
return self._conftest.rget(name, path)
|
||||
|
||||
def getoption(self, name):
|
||||
def getoption(self, name, default=notset, skip=False):
|
||||
""" return command line option value.
|
||||
|
||||
:arg name: name of the option. You may also specify
|
||||
the literal ``--OPT`` option instead of the "dest" option name.
|
||||
:arg default: default value if no option of that name exists.
|
||||
:arg skip: if True raise pytest.skip if option does not exists
|
||||
or has a None value.
|
||||
"""
|
||||
name = self._opt2dest.get(name, name)
|
||||
try:
|
||||
return getattr(self.option, name)
|
||||
val = getattr(self.option, name)
|
||||
if val is None and skip:
|
||||
raise AttributeError(name)
|
||||
return val
|
||||
except AttributeError:
|
||||
if default is not notset:
|
||||
return default
|
||||
if skip:
|
||||
import pytest
|
||||
pytest.skip("no %r option found" %(name,))
|
||||
raise ValueError("no option named %r" % (name,))
|
||||
|
||||
def getvalue(self, name, path=None):
|
||||
""" return command line option value.
|
||||
|
||||
:arg name: name of the command line option
|
||||
|
||||
(deprecated) if we can't find the option also lookup
|
||||
the name in a matching conftest file.
|
||||
"""
|
||||
try:
|
||||
return getattr(self.option, name)
|
||||
except AttributeError:
|
||||
return self._getconftest(name, path, check=False)
|
||||
""" (deprecated, use getoption()) """
|
||||
return self.getoption(name)
|
||||
|
||||
def getvalueorskip(self, name, path=None):
|
||||
""" (deprecated) return getvalue(name) or call
|
||||
py.test.skip if no value exists. """
|
||||
__tracebackhide__ = True
|
||||
try:
|
||||
val = self.getvalue(name, path)
|
||||
if val is None:
|
||||
raise KeyError(name)
|
||||
return val
|
||||
except KeyError:
|
||||
py.test.skip("no %r value found" %(name,))
|
||||
""" (deprecated, use getoption(skip=True)) """
|
||||
return self.getoption(name, skip=True)
|
||||
|
||||
def exists(path, ignore=EnvironmentError):
|
||||
try:
|
||||
@@ -837,8 +860,58 @@ def getcfg(args, inibasenames):
|
||||
if exists(p):
|
||||
iniconfig = py.iniconfig.IniConfig(p)
|
||||
if 'pytest' in iniconfig.sections:
|
||||
return iniconfig['pytest']
|
||||
return {}
|
||||
return base, p, iniconfig['pytest']
|
||||
elif inibasename == "pytest.ini":
|
||||
# allowed to be empty
|
||||
return base, p, {}
|
||||
return None, None, None
|
||||
|
||||
|
||||
def get_common_ancestor(args):
|
||||
# args are what we get after early command line parsing (usually
|
||||
# strings, but can be py.path.local objects as well)
|
||||
common_ancestor = None
|
||||
for arg in args:
|
||||
if str(arg)[0] == "-":
|
||||
continue
|
||||
p = py.path.local(arg)
|
||||
if common_ancestor is None:
|
||||
common_ancestor = p
|
||||
else:
|
||||
if p.relto(common_ancestor) or p == common_ancestor:
|
||||
continue
|
||||
elif common_ancestor.relto(p):
|
||||
common_ancestor = p
|
||||
else:
|
||||
shared = p.common(common_ancestor)
|
||||
if shared is not None:
|
||||
common_ancestor = shared
|
||||
if common_ancestor is None:
|
||||
common_ancestor = py.path.local()
|
||||
elif not common_ancestor.isdir():
|
||||
common_ancestor = common_ancestor.dirpath()
|
||||
return common_ancestor
|
||||
|
||||
|
||||
def determine_setup(inifile, args):
|
||||
if inifile:
|
||||
iniconfig = py.iniconfig.IniConfig(inifile)
|
||||
try:
|
||||
inicfg = iniconfig["pytest"]
|
||||
except KeyError:
|
||||
inicfg = None
|
||||
rootdir = get_common_ancestor(args)
|
||||
else:
|
||||
ancestor = get_common_ancestor(args)
|
||||
rootdir, inifile, inicfg = getcfg(
|
||||
[ancestor], ["pytest.ini", "tox.ini", "setup.cfg"])
|
||||
if rootdir is None:
|
||||
for rootdir in ancestor.parts(reverse=True):
|
||||
if rootdir.join("setup.py").exists():
|
||||
break
|
||||
else:
|
||||
rootdir = ancestor
|
||||
return rootdir, inifile, inicfg or {}
|
||||
|
||||
|
||||
def setns(obj, dic):
|
||||
@@ -848,7 +921,7 @@ def setns(obj, dic):
|
||||
mod = getattr(obj, name, None)
|
||||
if mod is None:
|
||||
modname = "pytest.%s" % name
|
||||
mod = py.std.types.ModuleType(modname)
|
||||
mod = types.ModuleType(modname)
|
||||
sys.modules[modname] = mod
|
||||
mod.__all__ = []
|
||||
setattr(obj, name, mod)
|
||||
|
||||
350
_pytest/core.py
350
_pytest/core.py
@@ -1,13 +1,17 @@
|
||||
"""
|
||||
pytest PluginManager, basic initialization and tracing.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import inspect
|
||||
import py
|
||||
# don't import pytest to avoid circular imports
|
||||
|
||||
assert py.__version__.split(".")[:2] >= ['1', '4'], ("installation problem: "
|
||||
"%s is too old, remove or upgrade 'py'" % (py.__version__))
|
||||
|
||||
py3 = sys.version_info > (3,0)
|
||||
|
||||
class TagTracer:
|
||||
def __init__(self):
|
||||
self._tag2proc = {}
|
||||
@@ -65,16 +69,104 @@ class TagTracerSub:
|
||||
def get(self, name):
|
||||
return self.__class__(self.root, self.tags + (name,))
|
||||
|
||||
|
||||
def add_method_wrapper(cls, wrapper_func):
|
||||
""" Substitute the function named "wrapperfunc.__name__" at class
|
||||
"cls" with a function that wraps the call to the original function.
|
||||
Return an undo function which can be called to reset the class to use
|
||||
the old method again.
|
||||
|
||||
wrapper_func is called with the same arguments as the method
|
||||
it wraps and its result is used as a wrap_controller for
|
||||
calling the original function.
|
||||
"""
|
||||
name = wrapper_func.__name__
|
||||
oldcall = getattr(cls, name)
|
||||
def wrap_exec(*args, **kwargs):
|
||||
gen = wrapper_func(*args, **kwargs)
|
||||
return wrapped_call(gen, lambda: oldcall(*args, **kwargs))
|
||||
|
||||
setattr(cls, name, wrap_exec)
|
||||
return lambda: setattr(cls, name, oldcall)
|
||||
|
||||
def raise_wrapfail(wrap_controller, msg):
|
||||
co = wrap_controller.gi_code
|
||||
raise RuntimeError("wrap_controller at %r %s:%d %s" %
|
||||
(co.co_name, co.co_filename, co.co_firstlineno, msg))
|
||||
|
||||
def wrapped_call(wrap_controller, func):
|
||||
""" Wrap calling to a function with a generator which needs to yield
|
||||
exactly once. The yield point will trigger calling the wrapped function
|
||||
and return its CallOutcome to the yield point. The generator then needs
|
||||
to finish (raise StopIteration) in order for the wrapped call to complete.
|
||||
"""
|
||||
try:
|
||||
next(wrap_controller) # first yield
|
||||
except StopIteration:
|
||||
raise_wrapfail(wrap_controller, "did not yield")
|
||||
call_outcome = CallOutcome(func)
|
||||
try:
|
||||
wrap_controller.send(call_outcome)
|
||||
raise_wrapfail(wrap_controller, "has second yield")
|
||||
except StopIteration:
|
||||
pass
|
||||
return call_outcome.get_result()
|
||||
|
||||
|
||||
class CallOutcome:
|
||||
""" Outcome of a function call, either an exception or a proper result.
|
||||
Calling the ``get_result`` method will return the result or reraise
|
||||
the exception raised when the function was called. """
|
||||
excinfo = None
|
||||
def __init__(self, func):
|
||||
try:
|
||||
self.result = func()
|
||||
except BaseException:
|
||||
self.excinfo = sys.exc_info()
|
||||
|
||||
def force_result(self, result):
|
||||
self.result = result
|
||||
self.excinfo = None
|
||||
|
||||
def get_result(self):
|
||||
if self.excinfo is None:
|
||||
return self.result
|
||||
else:
|
||||
ex = self.excinfo
|
||||
if py3:
|
||||
raise ex[1].with_traceback(ex[2])
|
||||
py.builtin._reraise(*ex)
|
||||
|
||||
|
||||
class PluginManager(object):
|
||||
def __init__(self, hookspecs=None):
|
||||
def __init__(self, hookspecs=None, prefix="pytest_"):
|
||||
self._name2plugin = {}
|
||||
self._listattrcache = {}
|
||||
self._plugins = []
|
||||
self._hints = []
|
||||
self._conftestplugins = []
|
||||
self._plugin2hookcallers = {}
|
||||
self._warnings = []
|
||||
self.trace = TagTracer().get("pluginmanage")
|
||||
self._plugin_distinfo = []
|
||||
self._shutdown = []
|
||||
self.hook = HookRelay(hookspecs or [], pm=self)
|
||||
self.hook = HookRelay(hookspecs or [], pm=self, prefix=prefix)
|
||||
|
||||
def set_tracing(self, writer):
|
||||
self.trace.root.setwriter(writer)
|
||||
# reconfigure HookCalling to perform tracing
|
||||
assert not hasattr(self, "_wrapping")
|
||||
self._wrapping = True
|
||||
|
||||
def _docall(self, methods, kwargs):
|
||||
trace = self.hookrelay.trace
|
||||
trace.root.indent += 1
|
||||
trace(self.name, kwargs)
|
||||
box = yield
|
||||
if box.excinfo is None:
|
||||
trace("finish", self.name, "-->", box.result)
|
||||
trace.root.indent -= 1
|
||||
|
||||
undo = add_method_wrapper(HookCaller, _docall)
|
||||
self.add_shutdown(undo)
|
||||
|
||||
def do_configure(self, config):
|
||||
# backward compatibility
|
||||
@@ -84,7 +176,7 @@ class PluginManager(object):
|
||||
assert not hasattr(self, "_registercallback")
|
||||
self._registercallback = callback
|
||||
|
||||
def register(self, plugin, name=None, prepend=False):
|
||||
def register(self, plugin, name=None, prepend=False, conftest=False):
|
||||
if self._name2plugin.get(name, None) == -1:
|
||||
return
|
||||
name = name or getattr(plugin, '__name__', str(id(plugin)))
|
||||
@@ -92,23 +184,35 @@ class PluginManager(object):
|
||||
raise ValueError("Plugin already registered: %s=%s\n%s" %(
|
||||
name, plugin, self._name2plugin))
|
||||
#self.trace("registering", name, plugin)
|
||||
self._name2plugin[name] = plugin
|
||||
reg = getattr(self, "_registercallback", None)
|
||||
if reg is not None:
|
||||
reg(plugin, name)
|
||||
if not prepend:
|
||||
self._plugins.append(plugin)
|
||||
reg(plugin, name) # may call addhooks
|
||||
hookcallers = list(self.hook._scan_plugin(plugin))
|
||||
self._plugin2hookcallers[plugin] = hookcallers
|
||||
self._name2plugin[name] = plugin
|
||||
if conftest:
|
||||
self._conftestplugins.append(plugin)
|
||||
else:
|
||||
self._plugins.insert(0, plugin)
|
||||
if not prepend:
|
||||
self._plugins.append(plugin)
|
||||
else:
|
||||
self._plugins.insert(0, plugin)
|
||||
# finally make sure that the methods of the new plugin take part
|
||||
for hookcaller in hookcallers:
|
||||
hookcaller.scan_methods()
|
||||
return True
|
||||
|
||||
def unregister(self, plugin=None, name=None):
|
||||
if plugin is None:
|
||||
plugin = self.getplugin(name=name)
|
||||
self._plugins.remove(plugin)
|
||||
def unregister(self, plugin):
|
||||
try:
|
||||
self._plugins.remove(plugin)
|
||||
except KeyError:
|
||||
self._conftestplugins.remove(plugin)
|
||||
for name, value in list(self._name2plugin.items()):
|
||||
if value == plugin:
|
||||
del self._name2plugin[name]
|
||||
hookcallers = self._plugin2hookcallers.pop(plugin)
|
||||
for hookcaller in hookcallers:
|
||||
hookcaller.scan_methods()
|
||||
|
||||
def add_shutdown(self, func):
|
||||
self._shutdown.append(func)
|
||||
@@ -117,26 +221,24 @@ class PluginManager(object):
|
||||
while self._shutdown:
|
||||
func = self._shutdown.pop()
|
||||
func()
|
||||
self._plugins = []
|
||||
self._plugins = self._conftestplugins = []
|
||||
self._name2plugin.clear()
|
||||
self._listattrcache.clear()
|
||||
|
||||
def isregistered(self, plugin, name=None):
|
||||
if self.getplugin(name) is not None:
|
||||
return True
|
||||
for val in self._name2plugin.values():
|
||||
if plugin == val:
|
||||
return True
|
||||
return plugin in self._plugins or plugin in self._conftestplugins
|
||||
|
||||
def addhooks(self, spec, prefix="pytest_"):
|
||||
self.hook._addhooks(spec, prefix=prefix)
|
||||
|
||||
def getplugins(self):
|
||||
return list(self._plugins)
|
||||
return self._plugins + self._conftestplugins
|
||||
|
||||
def skipifmissing(self, name):
|
||||
if not self.hasplugin(name):
|
||||
py.test.skip("plugin %r is missing" % name)
|
||||
import pytest
|
||||
pytest.skip("plugin %r is missing" % name)
|
||||
|
||||
def hasplugin(self, name):
|
||||
return bool(self.getplugin(name))
|
||||
@@ -152,7 +254,7 @@ class PluginManager(object):
|
||||
# API for bootstrapping
|
||||
#
|
||||
def _envlist(self, varname):
|
||||
val = py.std.os.environ.get(varname, None)
|
||||
val = os.environ.get(varname, None)
|
||||
if val is not None:
|
||||
return val.split(',')
|
||||
return ()
|
||||
@@ -187,15 +289,17 @@ class PluginManager(object):
|
||||
def consider_pluginarg(self, arg):
|
||||
if arg.startswith("no:"):
|
||||
name = arg[3:]
|
||||
if self.getplugin(name) is not None:
|
||||
self.unregister(None, name=name)
|
||||
plugin = self.getplugin(name)
|
||||
if plugin is not None:
|
||||
self.unregister(plugin)
|
||||
self._name2plugin[name] = -1
|
||||
else:
|
||||
if self.getplugin(arg) is None:
|
||||
self.import_plugin(arg)
|
||||
|
||||
def consider_conftest(self, conftestmodule):
|
||||
if self.register(conftestmodule, name=conftestmodule.__file__):
|
||||
if self.register(conftestmodule, name=conftestmodule.__file__,
|
||||
conftest=True):
|
||||
self.consider_module(conftestmodule)
|
||||
|
||||
def consider_module(self, mod):
|
||||
@@ -219,39 +323,36 @@ class PluginManager(object):
|
||||
return self.import_plugin(modname[7:])
|
||||
raise
|
||||
except:
|
||||
e = py.std.sys.exc_info()[1]
|
||||
if not hasattr(py.test, 'skip'):
|
||||
e = sys.exc_info()[1]
|
||||
import pytest
|
||||
if not hasattr(pytest, 'skip') or not isinstance(e, pytest.skip.Exception):
|
||||
raise
|
||||
elif not isinstance(e, py.test.skip.Exception):
|
||||
raise
|
||||
self._hints.append("skipped plugin %r: %s" %((modname, e.msg)))
|
||||
self._warnings.append("skipped plugin %r: %s" %((modname, e.msg)))
|
||||
else:
|
||||
self.register(mod, modname)
|
||||
self.consider_module(mod)
|
||||
|
||||
def listattr(self, attrname, plugins=None):
|
||||
if plugins is None:
|
||||
plugins = self._plugins
|
||||
key = (attrname,) + tuple(plugins)
|
||||
try:
|
||||
return list(self._listattrcache[key])
|
||||
except KeyError:
|
||||
pass
|
||||
plugins = self._plugins + self._conftestplugins
|
||||
l = []
|
||||
last = []
|
||||
wrappers = []
|
||||
for plugin in plugins:
|
||||
try:
|
||||
meth = getattr(plugin, attrname)
|
||||
if hasattr(meth, 'tryfirst'):
|
||||
last.append(meth)
|
||||
elif hasattr(meth, 'trylast'):
|
||||
l.insert(0, meth)
|
||||
else:
|
||||
l.append(meth)
|
||||
except AttributeError:
|
||||
continue
|
||||
if hasattr(meth, 'hookwrapper'):
|
||||
wrappers.append(meth)
|
||||
elif hasattr(meth, 'tryfirst'):
|
||||
last.append(meth)
|
||||
elif hasattr(meth, 'trylast'):
|
||||
l.insert(0, meth)
|
||||
else:
|
||||
l.append(meth)
|
||||
l.extend(last)
|
||||
self._listattrcache[key] = list(l)
|
||||
l.extend(wrappers)
|
||||
return l
|
||||
|
||||
def call_plugin(self, plugin, methname, kwargs):
|
||||
@@ -263,26 +364,19 @@ def importplugin(importspec):
|
||||
name = importspec
|
||||
try:
|
||||
mod = "_pytest." + name
|
||||
#print >>sys.stderr, "tryimport", mod
|
||||
__import__(mod)
|
||||
return sys.modules[mod]
|
||||
except ImportError:
|
||||
#e = py.std.sys.exc_info()[1]
|
||||
#if str(e).find(name) == -1:
|
||||
# raise
|
||||
pass #
|
||||
try:
|
||||
#print >>sys.stderr, "tryimport", importspec
|
||||
__import__(importspec)
|
||||
except ImportError:
|
||||
raise ImportError(importspec)
|
||||
return sys.modules[importspec]
|
||||
return sys.modules[importspec]
|
||||
|
||||
class MultiCall:
|
||||
""" execute a call into multiple python functions/methods. """
|
||||
|
||||
def __init__(self, methods, kwargs, firstresult=False):
|
||||
self.methods = list(methods)
|
||||
self.kwargs = kwargs
|
||||
self.kwargs["__multicall__"] = self
|
||||
self.results = []
|
||||
self.firstresult = firstresult
|
||||
|
||||
@@ -291,10 +385,13 @@ class MultiCall:
|
||||
return "<MultiCall %s, kwargs=%r>" %(status, self.kwargs)
|
||||
|
||||
def execute(self):
|
||||
all_kwargs = self.kwargs
|
||||
while self.methods:
|
||||
method = self.methods.pop()
|
||||
kwargs = self.getkwargs(method)
|
||||
res = method(**kwargs)
|
||||
args = [all_kwargs[argname] for argname in varnames(method)]
|
||||
if hasattr(method, "hookwrapper"):
|
||||
return wrapped_call(method(*args), self.execute)
|
||||
res = method(*args)
|
||||
if res is not None:
|
||||
self.results.append(res)
|
||||
if self.firstresult:
|
||||
@@ -302,84 +399,145 @@ class MultiCall:
|
||||
if not self.firstresult:
|
||||
return self.results
|
||||
|
||||
def getkwargs(self, method):
|
||||
kwargs = {}
|
||||
for argname in varnames(method):
|
||||
try:
|
||||
kwargs[argname] = self.kwargs[argname]
|
||||
except KeyError:
|
||||
if argname == "__multicall__":
|
||||
kwargs[argname] = self
|
||||
return kwargs
|
||||
|
||||
def varnames(func):
|
||||
def varnames(func, startindex=None):
|
||||
""" return argument name tuple for a function, method, class or callable.
|
||||
|
||||
In case of a class, its "__init__" method is considered.
|
||||
For methods the "self" parameter is not included unless you are passing
|
||||
an unbound method with Python3 (which has no supports for unbound methods)
|
||||
"""
|
||||
cache = getattr(func, "__dict__", {})
|
||||
try:
|
||||
return func._varnames
|
||||
except AttributeError:
|
||||
return cache["_varnames"]
|
||||
except KeyError:
|
||||
pass
|
||||
if not inspect.isfunction(func) and not inspect.ismethod(func):
|
||||
func = getattr(func, '__call__', func)
|
||||
ismethod = inspect.ismethod(func)
|
||||
if inspect.isclass(func):
|
||||
try:
|
||||
func = func.__init__
|
||||
except AttributeError:
|
||||
return ()
|
||||
startindex = 1
|
||||
else:
|
||||
if not inspect.isfunction(func) and not inspect.ismethod(func):
|
||||
func = getattr(func, '__call__', func)
|
||||
if startindex is None:
|
||||
startindex = int(inspect.ismethod(func))
|
||||
|
||||
rawcode = py.code.getrawcode(func)
|
||||
try:
|
||||
x = rawcode.co_varnames[ismethod:rawcode.co_argcount]
|
||||
x = rawcode.co_varnames[startindex:rawcode.co_argcount]
|
||||
except AttributeError:
|
||||
x = ()
|
||||
py.builtin._getfuncdict(func)['_varnames'] = x
|
||||
else:
|
||||
defaults = func.__defaults__
|
||||
if defaults:
|
||||
x = x[:-len(defaults)]
|
||||
try:
|
||||
cache["_varnames"] = x
|
||||
except TypeError:
|
||||
pass
|
||||
return x
|
||||
|
||||
|
||||
class HookRelay:
|
||||
def __init__(self, hookspecs, pm, prefix="pytest_"):
|
||||
if not isinstance(hookspecs, list):
|
||||
hookspecs = [hookspecs]
|
||||
self._hookspecs = []
|
||||
self._pm = pm
|
||||
self.trace = pm.trace.root.get("hook")
|
||||
self.prefix = prefix
|
||||
for hookspec in hookspecs:
|
||||
self._addhooks(hookspec, prefix)
|
||||
|
||||
def _addhooks(self, hookspecs, prefix):
|
||||
self._hookspecs.append(hookspecs)
|
||||
def _addhooks(self, hookspec, prefix):
|
||||
added = False
|
||||
for name, method in vars(hookspecs).items():
|
||||
isclass = int(inspect.isclass(hookspec))
|
||||
for name, method in vars(hookspec).items():
|
||||
if name.startswith(prefix):
|
||||
firstresult = getattr(method, 'firstresult', False)
|
||||
hc = HookCaller(self, name, firstresult=firstresult)
|
||||
hc = HookCaller(self, name, firstresult=firstresult,
|
||||
argnames=varnames(method, startindex=isclass))
|
||||
setattr(self, name, hc)
|
||||
added = True
|
||||
#print ("setting new hook", name)
|
||||
if not added:
|
||||
raise ValueError("did not find new %r hooks in %r" %(
|
||||
prefix, hookspecs,))
|
||||
prefix, hookspec,))
|
||||
|
||||
def _getcaller(self, name, plugins):
|
||||
caller = getattr(self, name)
|
||||
methods = self._pm.listattr(name, plugins=plugins)
|
||||
if methods:
|
||||
return caller.new_cached_caller(methods)
|
||||
return caller
|
||||
|
||||
def _scan_plugin(self, plugin):
|
||||
def fail(msg, *args):
|
||||
name = getattr(plugin, '__name__', plugin)
|
||||
raise PluginValidationError("plugin %r\n%s" %(name, msg % args))
|
||||
|
||||
for name in dir(plugin):
|
||||
if not name.startswith(self.prefix):
|
||||
continue
|
||||
hook = getattr(self, name, None)
|
||||
method = getattr(plugin, name)
|
||||
if hook is None:
|
||||
is_optional = getattr(method, 'optionalhook', False)
|
||||
if not isgenerichook(name) and not is_optional:
|
||||
fail("found unknown hook: %r", name)
|
||||
continue
|
||||
for arg in varnames(method):
|
||||
if arg not in hook.argnames:
|
||||
fail("argument %r not available\n"
|
||||
"actual definition: %s\n"
|
||||
"available hookargs: %s",
|
||||
arg, formatdef(method),
|
||||
", ".join(hook.argnames))
|
||||
yield hook
|
||||
|
||||
|
||||
class HookCaller:
|
||||
def __init__(self, hookrelay, name, firstresult):
|
||||
def __init__(self, hookrelay, name, firstresult, argnames, methods=()):
|
||||
self.hookrelay = hookrelay
|
||||
self.name = name
|
||||
self.firstresult = firstresult
|
||||
self.trace = self.hookrelay.trace
|
||||
self.argnames = ["__multicall__"]
|
||||
self.argnames.extend(argnames)
|
||||
assert "self" not in argnames # sanity check
|
||||
self.methods = methods
|
||||
|
||||
def new_cached_caller(self, methods):
|
||||
return HookCaller(self.hookrelay, self.name, self.firstresult,
|
||||
argnames=self.argnames, methods=methods)
|
||||
|
||||
def __repr__(self):
|
||||
return "<HookCaller %r>" %(self.name,)
|
||||
|
||||
def __call__(self, **kwargs):
|
||||
methods = self.hookrelay._pm.listattr(self.name)
|
||||
return self._docall(methods, kwargs)
|
||||
def scan_methods(self):
|
||||
self.methods = self.hookrelay._pm.listattr(self.name)
|
||||
|
||||
def pcall(self, plugins, **kwargs):
|
||||
methods = self.hookrelay._pm.listattr(self.name, plugins=plugins)
|
||||
return self._docall(methods, kwargs)
|
||||
def __call__(self, **kwargs):
|
||||
return self._docall(self.methods, kwargs)
|
||||
|
||||
def callextra(self, methods, **kwargs):
|
||||
return self._docall(self.methods + methods, kwargs)
|
||||
|
||||
def _docall(self, methods, kwargs):
|
||||
self.trace(self.name, kwargs)
|
||||
self.trace.root.indent += 1
|
||||
mc = MultiCall(methods, kwargs, firstresult=self.firstresult)
|
||||
try:
|
||||
res = mc.execute()
|
||||
if res:
|
||||
self.trace("finish", self.name, "-->", res)
|
||||
finally:
|
||||
self.trace.root.indent -= 1
|
||||
return res
|
||||
return MultiCall(methods, kwargs,
|
||||
firstresult=self.firstresult).execute()
|
||||
|
||||
|
||||
class PluginValidationError(Exception):
|
||||
""" plugin failed validation. """
|
||||
|
||||
def isgenerichook(name):
|
||||
return name == "pytest_plugins" or \
|
||||
name.startswith("pytest_funcarg__")
|
||||
|
||||
def formatdef(func):
|
||||
return "%s%s" % (
|
||||
func.__name__,
|
||||
inspect.formatargspec(*inspect.getargspec(func))
|
||||
)
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
""" discover and run doctests in modules and test files."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
import traceback
|
||||
import pytest, py
|
||||
from _pytest.python import FixtureRequest, FuncFixtureInfo
|
||||
from py._code.code import TerminalRepr, ReprFileLocation
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addini('doctest_optionflags', 'option flags for doctests',
|
||||
type="args", default=["ELLIPSIS"])
|
||||
group = parser.getgroup("collect")
|
||||
group.addoption("--doctest-modules",
|
||||
action="store_true", default=False,
|
||||
@@ -14,6 +17,10 @@ def pytest_addoption(parser):
|
||||
action="store", default="test*.txt", metavar="pat",
|
||||
help="doctests file matching pattern, default: test*.txt",
|
||||
dest="doctestglob")
|
||||
group.addoption("--doctest-ignore-import-errors",
|
||||
action="store_true", default=False,
|
||||
help="ignore doctest ImportErrors",
|
||||
dest="doctest_ignore_import_errors")
|
||||
|
||||
def pytest_collect_file(path, parent):
|
||||
config = parent.config
|
||||
@@ -43,7 +50,7 @@ class DoctestItem(pytest.Item):
|
||||
self.runner.run(self.dtest)
|
||||
|
||||
def repr_failure(self, excinfo):
|
||||
doctest = py.std.doctest
|
||||
import doctest
|
||||
if excinfo.errisinstance((doctest.DocTestFailure,
|
||||
doctest.UnexpectedException)):
|
||||
doctestfailure = excinfo.value
|
||||
@@ -56,8 +63,8 @@ class DoctestItem(pytest.Item):
|
||||
lineno = test.lineno + example.lineno + 1
|
||||
message = excinfo.type.__name__
|
||||
reprlocation = ReprFileLocation(filename, lineno, message)
|
||||
checker = py.std.doctest.OutputChecker()
|
||||
REPORT_UDIFF = py.std.doctest.REPORT_UDIFF
|
||||
checker = doctest.OutputChecker()
|
||||
REPORT_UDIFF = doctest.REPORT_UDIFF
|
||||
filelines = py.path.local(filename).readlines(cr=0)
|
||||
lines = []
|
||||
if lineno is not None:
|
||||
@@ -78,7 +85,7 @@ class DoctestItem(pytest.Item):
|
||||
inner_excinfo = py.code.ExceptionInfo(excinfo.value.exc_info)
|
||||
lines += ["UNEXPECTED EXCEPTION: %s" %
|
||||
repr(inner_excinfo.value)]
|
||||
lines += py.std.traceback.format_exception(*excinfo.value.exc_info)
|
||||
lines += traceback.format_exception(*excinfo.value.exc_info)
|
||||
return ReprFailDoctest(reprlocation, lines)
|
||||
else:
|
||||
return super(DoctestItem, self).repr_failure(excinfo)
|
||||
@@ -86,26 +93,54 @@ class DoctestItem(pytest.Item):
|
||||
def reportinfo(self):
|
||||
return self.fspath, None, "[doctest] %s" % self.name
|
||||
|
||||
def _get_flag_lookup():
|
||||
import doctest
|
||||
return dict(DONT_ACCEPT_TRUE_FOR_1=doctest.DONT_ACCEPT_TRUE_FOR_1,
|
||||
DONT_ACCEPT_BLANKLINE=doctest.DONT_ACCEPT_BLANKLINE,
|
||||
NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE,
|
||||
ELLIPSIS=doctest.ELLIPSIS,
|
||||
IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL,
|
||||
COMPARISON_FLAGS=doctest.COMPARISON_FLAGS)
|
||||
|
||||
def get_optionflags(parent):
|
||||
optionflags_str = parent.config.getini("doctest_optionflags")
|
||||
flag_lookup_table = _get_flag_lookup()
|
||||
flag_acc = 0
|
||||
for flag in optionflags_str:
|
||||
flag_acc |= flag_lookup_table[flag]
|
||||
return flag_acc
|
||||
|
||||
class DoctestTextfile(DoctestItem, pytest.File):
|
||||
def runtest(self):
|
||||
doctest = py.std.doctest
|
||||
import doctest
|
||||
# satisfy `FixtureRequest` constructor...
|
||||
self.funcargs = {}
|
||||
self._fixtureinfo = FuncFixtureInfo((), [], {})
|
||||
fm = self.session._fixturemanager
|
||||
def func():
|
||||
pass
|
||||
self._fixtureinfo = fm.getfixtureinfo(node=self, func=func,
|
||||
cls=None, funcargs=False)
|
||||
fixture_request = FixtureRequest(self)
|
||||
fixture_request._fillfixtures()
|
||||
failed, tot = doctest.testfile(
|
||||
str(self.fspath), module_relative=False,
|
||||
optionflags=doctest.ELLIPSIS,
|
||||
optionflags=get_optionflags(self),
|
||||
extraglobs=dict(getfixture=fixture_request.getfuncargvalue),
|
||||
raise_on_error=True, verbose=0)
|
||||
|
||||
class DoctestModule(pytest.File):
|
||||
def collect(self):
|
||||
doctest = py.std.doctest
|
||||
import doctest
|
||||
if self.fspath.basename == "conftest.py":
|
||||
module = self.config._conftest.importconftest(self.fspath)
|
||||
else:
|
||||
module = self.fspath.pyimport()
|
||||
try:
|
||||
module = self.fspath.pyimport()
|
||||
except ImportError:
|
||||
if self.config.getvalue('doctest_ignore_import_errors'):
|
||||
pytest.skip('unable to import module %r' % self.fspath)
|
||||
else:
|
||||
raise
|
||||
# satisfy `FixtureRequest` constructor...
|
||||
self.funcargs = {}
|
||||
self._fixtureinfo = FuncFixtureInfo((), [], {})
|
||||
@@ -113,8 +148,9 @@ class DoctestModule(pytest.File):
|
||||
doctest_globals = dict(getfixture=fixture_request.getfuncargvalue)
|
||||
# uses internal doctest module parsing mechanism
|
||||
finder = doctest.DocTestFinder()
|
||||
runner = doctest.DebugRunner(verbose=0, optionflags=doctest.ELLIPSIS)
|
||||
optionflags = get_optionflags(self)
|
||||
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags)
|
||||
for test in finder.find(module, module.__name__,
|
||||
extraglobs=doctest_globals):
|
||||
if test.examples: # skip empty doctests
|
||||
if test.examples: # skip empty doctests
|
||||
yield DoctestItem(test.name, self, runner, test)
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
""" generate a single-file self-contained version of py.test """
|
||||
import py
|
||||
""" generate a single-file self-contained version of pytest """
|
||||
import os
|
||||
import sys
|
||||
import pkgutil
|
||||
|
||||
import py
|
||||
|
||||
import _pytest
|
||||
|
||||
|
||||
|
||||
def find_toplevel(name):
|
||||
for syspath in py.std.sys.path:
|
||||
for syspath in sys.path:
|
||||
base = py.path.local(syspath)
|
||||
lib = base/name
|
||||
if lib.check(dir=1):
|
||||
@@ -29,9 +36,10 @@ def pkg_to_mapping(name):
|
||||
return name2src
|
||||
|
||||
def compress_mapping(mapping):
|
||||
data = py.std.pickle.dumps(mapping, 2)
|
||||
data = py.std.zlib.compress(data, 9)
|
||||
data = py.std.base64.encodestring(data)
|
||||
import base64, pickle, zlib
|
||||
data = pickle.dumps(mapping, 2)
|
||||
data = zlib.compress(data, 9)
|
||||
data = base64.encodestring(data)
|
||||
data = data.decode('ascii')
|
||||
return data
|
||||
|
||||
@@ -55,7 +63,7 @@ def pytest_addoption(parser):
|
||||
group = parser.getgroup("debugconfig")
|
||||
group.addoption("--genscript", action="store", default=None,
|
||||
dest="genscript", metavar="path",
|
||||
help="create standalone py.test script at given target path.")
|
||||
help="create standalone pytest script at given target path.")
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
genscript = config.getvalue("genscript")
|
||||
@@ -64,13 +72,13 @@ def pytest_cmdline_main(config):
|
||||
deps = ['py', '_pytest', 'pytest']
|
||||
if sys.version_info < (2,7):
|
||||
deps.append("argparse")
|
||||
tw.line("generated script will run on python2.5-python3.3++")
|
||||
tw.line("generated script will run on python2.6-python3.3++")
|
||||
else:
|
||||
tw.line("WARNING: generated script will not run on python2.6 "
|
||||
"or below due to 'argparse' dependency. Use python2.6 "
|
||||
"to generate a python2.5/6 compatible script", red=True)
|
||||
"due to 'argparse' dependency. Use python2.6 "
|
||||
"to generate a python2.6 compatible script", red=True)
|
||||
script = generate_script(
|
||||
'import py; raise SystemExit(py.test.cmdline.main())',
|
||||
'import pytest; raise SystemExit(pytest.cmdline.main())',
|
||||
deps,
|
||||
)
|
||||
genscript = py.path.local(genscript)
|
||||
@@ -78,3 +86,42 @@ def pytest_cmdline_main(config):
|
||||
tw.line("generated pytest standalone script: %s" % genscript,
|
||||
bold=True)
|
||||
return 0
|
||||
|
||||
|
||||
def pytest_namespace():
|
||||
return {'freeze_includes': freeze_includes}
|
||||
|
||||
|
||||
def freeze_includes():
|
||||
"""
|
||||
Returns a list of module names used by py.test that should be
|
||||
included by cx_freeze.
|
||||
"""
|
||||
result = list(_iter_all_modules(py))
|
||||
result += list(_iter_all_modules(_pytest))
|
||||
return result
|
||||
|
||||
|
||||
def _iter_all_modules(package, prefix=''):
|
||||
"""
|
||||
Iterates over the names of all modules that can be found in the given
|
||||
package, recursively.
|
||||
|
||||
Example:
|
||||
_iter_all_modules(_pytest) ->
|
||||
['_pytest.assertion.newinterpret',
|
||||
'_pytest.capture',
|
||||
'_pytest.core',
|
||||
...
|
||||
]
|
||||
"""
|
||||
if type(package) is not str:
|
||||
path, prefix = package.__path__[0], package.__name__ + '.'
|
||||
else:
|
||||
path = package
|
||||
for _, name, is_package in pkgutil.iter_modules([path]):
|
||||
if is_package:
|
||||
for m in _iter_all_modules(os.path.join(path, name), prefix=name + '.'):
|
||||
yield prefix + m
|
||||
else:
|
||||
yield prefix + name
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
""" version info, help messages, tracing configuration. """
|
||||
import py
|
||||
import pytest
|
||||
import os, inspect, sys
|
||||
from _pytest.core import varnames
|
||||
import os, sys
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup('debugconfig')
|
||||
@@ -12,7 +11,9 @@ def pytest_addoption(parser):
|
||||
help="show help message and configuration info")
|
||||
group._addoption('-p', action="append", dest="plugins", default = [],
|
||||
metavar="name",
|
||||
help="early-load given plugin (multi-allowed).")
|
||||
help="early-load given plugin (multi-allowed). "
|
||||
"To avoid loading of plugins, use the `no:` prefix, e.g. "
|
||||
"`no:doctest`.")
|
||||
group.addoption('--traceconfig', '--trace-config',
|
||||
action="store_true", default=False,
|
||||
help="trace considerations of conftest.py files."),
|
||||
@@ -21,18 +22,21 @@ def pytest_addoption(parser):
|
||||
help="store internal tracing debug information in 'pytestdebug.log'.")
|
||||
|
||||
|
||||
def pytest_cmdline_parse(__multicall__):
|
||||
config = __multicall__.execute()
|
||||
@pytest.mark.hookwrapper
|
||||
def pytest_cmdline_parse():
|
||||
outcome = yield
|
||||
config = outcome.get_result()
|
||||
if config.option.debug:
|
||||
path = os.path.abspath("pytestdebug.log")
|
||||
f = open(path, 'w')
|
||||
config._debugfile = f
|
||||
f.write("versions pytest-%s, py-%s, python-%s\ncwd=%s\nargs=%s\n\n" %(
|
||||
pytest.__version__, py.__version__, ".".join(map(str, sys.version_info)),
|
||||
f.write("versions pytest-%s, py-%s, "
|
||||
"python-%s\ncwd=%s\nargs=%s\n\n" %(
|
||||
pytest.__version__, py.__version__,
|
||||
".".join(map(str, sys.version_info)),
|
||||
os.getcwd(), config._origargs))
|
||||
config.trace.root.setwriter(f.write)
|
||||
config.pluginmanager.set_tracing(f.write)
|
||||
sys.stderr.write("writing pytestdebug information to %s\n" % path)
|
||||
return config
|
||||
|
||||
@pytest.mark.trylast
|
||||
def pytest_unconfigure(config):
|
||||
@@ -46,7 +50,7 @@ def pytest_unconfigure(config):
|
||||
def pytest_cmdline_main(config):
|
||||
if config.option.version:
|
||||
p = py.path.local(pytest.__file__)
|
||||
sys.stderr.write("This is py.test version %s, imported from %s\n" %
|
||||
sys.stderr.write("This is pytest version %s, imported from %s\n" %
|
||||
(pytest.__version__, p))
|
||||
plugininfo = getpluginversioninfo(config)
|
||||
if plugininfo:
|
||||
@@ -62,7 +66,6 @@ def pytest_cmdline_main(config):
|
||||
def showhelp(config):
|
||||
tw = py.io.TerminalWriter()
|
||||
tw.write(config._parser.optparser.format_help())
|
||||
tw.write(config._parser.optparser.format_epilog(None))
|
||||
tw.line()
|
||||
tw.line()
|
||||
#tw.sep( "=", "config file settings")
|
||||
@@ -82,16 +85,12 @@ def showhelp(config):
|
||||
#tw.sep("=")
|
||||
tw.line("to see available markers type: py.test --markers")
|
||||
tw.line("to see available fixtures type: py.test --fixtures")
|
||||
tw.line("(shown according to specified file_or_dir or current dir "
|
||||
"if not specified)")
|
||||
for warning in config.pluginmanager._warnings:
|
||||
tw.line("warning: %s" % (warning,), red=True)
|
||||
return
|
||||
|
||||
tw.line("conftest.py options:")
|
||||
tw.line()
|
||||
conftestitems = sorted(config._parser._conftestdict.items())
|
||||
for name, help in conftest_options + conftestitems:
|
||||
line = " %-15s %s" %(name, help)
|
||||
tw.line(line[:tw.fullwidth])
|
||||
tw.line()
|
||||
#tw.sep( "=")
|
||||
|
||||
conftest_options = [
|
||||
('pytest_plugins', 'list of plugin names to load'),
|
||||
@@ -120,7 +119,6 @@ def pytest_report_header(config):
|
||||
|
||||
if config.option.traceconfig:
|
||||
lines.append("active plugins:")
|
||||
plugins = []
|
||||
items = config.pluginmanager._name2plugin.items()
|
||||
for name, plugin in items:
|
||||
if hasattr(plugin, '__file__'):
|
||||
@@ -131,70 +129,3 @@ def pytest_report_header(config):
|
||||
return lines
|
||||
|
||||
|
||||
# =====================================================
|
||||
# validate plugin syntax and hooks
|
||||
# =====================================================
|
||||
|
||||
def pytest_plugin_registered(manager, plugin):
|
||||
methods = collectattr(plugin)
|
||||
hooks = {}
|
||||
for hookspec in manager.hook._hookspecs:
|
||||
hooks.update(collectattr(hookspec))
|
||||
|
||||
stringio = py.io.TextIO()
|
||||
def Print(*args):
|
||||
if args:
|
||||
stringio.write(" ".join(map(str, args)))
|
||||
stringio.write("\n")
|
||||
|
||||
fail = False
|
||||
while methods:
|
||||
name, method = methods.popitem()
|
||||
#print "checking", name
|
||||
if isgenerichook(name):
|
||||
continue
|
||||
if name not in hooks:
|
||||
if not getattr(method, 'optionalhook', False):
|
||||
Print("found unknown hook:", name)
|
||||
fail = True
|
||||
else:
|
||||
#print "checking", method
|
||||
method_args = list(varnames(method))
|
||||
if '__multicall__' in method_args:
|
||||
method_args.remove('__multicall__')
|
||||
hook = hooks[name]
|
||||
hookargs = varnames(hook)
|
||||
for arg in method_args:
|
||||
if arg not in hookargs:
|
||||
Print("argument %r not available" %(arg, ))
|
||||
Print("actual definition: %s" %(formatdef(method)))
|
||||
Print("available hook arguments: %s" %
|
||||
", ".join(hookargs))
|
||||
fail = True
|
||||
break
|
||||
#if not fail:
|
||||
# print "matching hook:", formatdef(method)
|
||||
if fail:
|
||||
name = getattr(plugin, '__name__', plugin)
|
||||
raise PluginValidationError("%s:\n%s" % (name, stringio.getvalue()))
|
||||
|
||||
class PluginValidationError(Exception):
|
||||
""" plugin failed validation. """
|
||||
|
||||
def isgenerichook(name):
|
||||
return name == "pytest_plugins" or \
|
||||
name.startswith("pytest_funcarg__")
|
||||
|
||||
def collectattr(obj):
|
||||
methods = {}
|
||||
for apiname in dir(obj):
|
||||
if apiname.startswith("pytest_"):
|
||||
methods[apiname] = getattr(obj, apiname)
|
||||
return methods
|
||||
|
||||
def formatdef(func):
|
||||
return "%s%s" % (
|
||||
func.__name__,
|
||||
inspect.formatargspec(*inspect.getargspec(func))
|
||||
)
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ def pytest_addhooks(pluginmanager):
|
||||
|
||||
def pytest_namespace():
|
||||
"""return dict of name->object to be made globally available in
|
||||
the py.test/pytest namespace. This hook is called before command
|
||||
line options are parsed.
|
||||
the pytest namespace. This hook is called before command line options
|
||||
are parsed.
|
||||
"""
|
||||
|
||||
def pytest_cmdline_parse(pluginmanager, args):
|
||||
@@ -53,8 +53,8 @@ def pytest_cmdline_main(config):
|
||||
pytest_cmdline_main.firstresult = True
|
||||
|
||||
def pytest_load_initial_conftests(args, early_config, parser):
|
||||
""" implements loading initial conftests.
|
||||
"""
|
||||
""" implements the loading of initial conftest files ahead
|
||||
of command line option parsing. """
|
||||
|
||||
def pytest_configure(config):
|
||||
""" called after command line options have been parsed
|
||||
@@ -142,7 +142,7 @@ def pytest_generate_tests(metafunc):
|
||||
# -------------------------------------------------------------------------
|
||||
# generic runtest related hooks
|
||||
# -------------------------------------------------------------------------
|
||||
def pytest_itemstart(item, node=None):
|
||||
def pytest_itemstart(item, node):
|
||||
""" (deprecated, use pytest_runtest_logstart). """
|
||||
|
||||
def pytest_runtest_protocol(item, nextitem):
|
||||
@@ -152,9 +152,9 @@ def pytest_runtest_protocol(item, nextitem):
|
||||
|
||||
:arg item: test item for which the runtest protocol is performed.
|
||||
|
||||
:arg nexitem: the scheduled-to-be-next test item (or None if this
|
||||
is the end my friend). This argument is passed on to
|
||||
:py:func:`pytest_runtest_teardown`.
|
||||
:arg nextitem: the scheduled-to-be-next test item (or None if this
|
||||
is the end my friend). This argument is passed on to
|
||||
:py:func:`pytest_runtest_teardown`.
|
||||
|
||||
:return boolean: True if no further hook implementations should be invoked.
|
||||
"""
|
||||
@@ -172,10 +172,10 @@ def pytest_runtest_call(item):
|
||||
def pytest_runtest_teardown(item, nextitem):
|
||||
""" called after ``pytest_runtest_call``.
|
||||
|
||||
:arg nexitem: the scheduled-to-be-next test item (None if no further
|
||||
test item is scheduled). This argument can be used to
|
||||
perform exact teardowns, i.e. calling just enough finalizers
|
||||
so that nextitem only needs to call setup-functions.
|
||||
:arg nextitem: the scheduled-to-be-next test item (None if no further
|
||||
test item is scheduled). This argument can be used to
|
||||
perform exact teardowns, i.e. calling just enough finalizers
|
||||
so that nextitem only needs to call setup-functions.
|
||||
"""
|
||||
|
||||
def pytest_runtest_makereport(item, call):
|
||||
@@ -227,6 +227,11 @@ pytest_report_teststatus.firstresult = True
|
||||
def pytest_terminal_summary(terminalreporter):
|
||||
""" add additional section in terminal summary reporting. """
|
||||
|
||||
def pytest_logwarning(message, code, nodeid, fslocation):
|
||||
""" process a warning specified by a message, a code string,
|
||||
a nodeid and fslocation (both of which may be None
|
||||
if the warning is not tied to a partilar node/location)."""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# doctest hooks
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -257,3 +262,5 @@ def pytest_exception_interact(node, call, report):
|
||||
that is not an internal exception like "skip.Exception".
|
||||
"""
|
||||
|
||||
def pytest_enter_pdb():
|
||||
""" called upon pdb.set_trace()"""
|
||||
|
||||
@@ -2,29 +2,20 @@
|
||||
|
||||
Based on initial code from Ross Lawley.
|
||||
"""
|
||||
|
||||
import py
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
# Python 2.X and 3.X compatibility
|
||||
try:
|
||||
unichr(65)
|
||||
except NameError:
|
||||
if sys.version_info[0] < 3:
|
||||
from codecs import open
|
||||
else:
|
||||
unichr = chr
|
||||
try:
|
||||
unicode('A')
|
||||
except NameError:
|
||||
unicode = str
|
||||
try:
|
||||
long(1)
|
||||
except NameError:
|
||||
long = int
|
||||
|
||||
|
||||
class Junit(py.xml.Namespace):
|
||||
pass
|
||||
|
||||
@@ -109,12 +100,14 @@ class LogXML(object):
|
||||
))
|
||||
|
||||
def _write_captured_output(self, report):
|
||||
sec = dict(report.sections)
|
||||
for name in ('out', 'err'):
|
||||
content = sec.get("Captured std%s" % name)
|
||||
if content:
|
||||
tag = getattr(Junit, 'system-'+name)
|
||||
self.append(tag(bin_xml_escape(content)))
|
||||
for capname in ('out', 'err'):
|
||||
allcontent = ""
|
||||
for name, content in report.get_sections("Captured std%s" %
|
||||
capname):
|
||||
allcontent += content
|
||||
if allcontent:
|
||||
tag = getattr(Junit, 'system-'+capname)
|
||||
self.append(tag(bin_xml_escape(allcontent)))
|
||||
|
||||
def append(self, obj):
|
||||
self.tests[-1].append(obj)
|
||||
@@ -130,39 +123,46 @@ class LogXML(object):
|
||||
Junit.skipped(message="xfail-marked test passes unexpectedly"))
|
||||
self.skipped += 1
|
||||
else:
|
||||
fail = Junit.failure(message="test failure")
|
||||
fail.append(str(report.longrepr))
|
||||
if hasattr(report.longrepr, "reprcrash"):
|
||||
message = report.longrepr.reprcrash.message
|
||||
elif isinstance(report.longrepr, (unicode, str)):
|
||||
message = report.longrepr
|
||||
else:
|
||||
message = str(report.longrepr)
|
||||
message = bin_xml_escape(message)
|
||||
fail = Junit.failure(message=message)
|
||||
fail.append(bin_xml_escape(report.longrepr))
|
||||
self.append(fail)
|
||||
self.failed += 1
|
||||
self._write_captured_output(report)
|
||||
|
||||
def append_collect_failure(self, report):
|
||||
def append_collect_error(self, report):
|
||||
#msg = str(report.longrepr.reprtraceback.extraline)
|
||||
self.append(Junit.failure(str(report.longrepr),
|
||||
message="collection failure"))
|
||||
self.append(Junit.error(bin_xml_escape(report.longrepr),
|
||||
message="collection failure"))
|
||||
self.errors += 1
|
||||
|
||||
def append_collect_skipped(self, report):
|
||||
#msg = str(report.longrepr.reprtraceback.extraline)
|
||||
self.append(Junit.skipped(str(report.longrepr),
|
||||
self.append(Junit.skipped(bin_xml_escape(report.longrepr),
|
||||
message="collection skipped"))
|
||||
self.skipped += 1
|
||||
|
||||
def append_error(self, report):
|
||||
self.append(Junit.error(str(report.longrepr),
|
||||
self.append(Junit.error(bin_xml_escape(report.longrepr),
|
||||
message="test setup failure"))
|
||||
self.errors += 1
|
||||
|
||||
def append_skipped(self, report):
|
||||
if hasattr(report, "wasxfail"):
|
||||
self.append(Junit.skipped(str(report.wasxfail),
|
||||
self.append(Junit.skipped(bin_xml_escape(report.wasxfail),
|
||||
message="expected test failure"))
|
||||
else:
|
||||
filename, lineno, skipreason = report.longrepr
|
||||
if skipreason.startswith("Skipped: "):
|
||||
skipreason = skipreason[9:]
|
||||
skipreason = bin_xml_escape(skipreason[9:])
|
||||
self.append(
|
||||
Junit.skipped("%s:%s: %s" % report.longrepr,
|
||||
Junit.skipped("%s:%s: %s" % (filename, lineno, skipreason),
|
||||
type="pytest.skip",
|
||||
message=skipreason
|
||||
))
|
||||
@@ -188,28 +188,27 @@ class LogXML(object):
|
||||
if not report.passed:
|
||||
self._opentestcase(report)
|
||||
if report.failed:
|
||||
self.append_collect_failure(report)
|
||||
self.append_collect_error(report)
|
||||
else:
|
||||
self.append_collect_skipped(report)
|
||||
|
||||
def pytest_internalerror(self, excrepr):
|
||||
self.errors += 1
|
||||
data = py.xml.escape(excrepr)
|
||||
data = bin_xml_escape(excrepr)
|
||||
self.tests.append(
|
||||
Junit.testcase(
|
||||
Junit.error(data, message="internal error"),
|
||||
classname="pytest",
|
||||
name="internal"))
|
||||
|
||||
def pytest_sessionstart(self, session):
|
||||
def pytest_sessionstart(self):
|
||||
self.suite_start_time = time.time()
|
||||
|
||||
def pytest_sessionfinish(self, session, exitstatus, __multicall__):
|
||||
if py.std.sys.version_info[0] < 3:
|
||||
logfile = py.std.codecs.open(self.logfile, 'w', encoding='utf-8')
|
||||
else:
|
||||
logfile = open(self.logfile, 'w', encoding='utf-8')
|
||||
|
||||
def pytest_sessionfinish(self):
|
||||
dirname = os.path.dirname(os.path.abspath(self.logfile))
|
||||
if not os.path.isdir(dirname):
|
||||
os.makedirs(dirname)
|
||||
logfile = open(self.logfile, 'w', encoding='utf-8')
|
||||
suite_stop_time = time.time()
|
||||
suite_time_delta = suite_stop_time - self.suite_start_time
|
||||
numtests = self.passed + self.failed
|
||||
|
||||
119
_pytest/main.py
119
_pytest/main.py
@@ -1,4 +1,5 @@
|
||||
""" core implementation of testing process: init, session, runtest loop. """
|
||||
import re
|
||||
|
||||
import py
|
||||
import pytest, _pytest
|
||||
@@ -8,7 +9,7 @@ try:
|
||||
except ImportError:
|
||||
from UserDict import DictMixin as MappingMixin
|
||||
|
||||
from _pytest.runner import collect_one_node, Skipped
|
||||
from _pytest.runner import collect_one_node
|
||||
|
||||
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
|
||||
|
||||
@@ -19,11 +20,11 @@ EXIT_INTERRUPTED = 2
|
||||
EXIT_INTERNALERROR = 3
|
||||
EXIT_USAGEERROR = 4
|
||||
|
||||
name_re = py.std.re.compile("^[a-zA-Z_]\w*$")
|
||||
name_re = re.compile("^[a-zA-Z_]\w*$")
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addini("norecursedirs", "directory patterns to avoid for recursion",
|
||||
type="args", default=('.*', 'CVS', '_darcs', '{arch}'))
|
||||
type="args", default=['.*', 'CVS', '_darcs', '{arch}', '*.egg'])
|
||||
#parser.addini("dirpatterns",
|
||||
# "patterns specifying possible locations of test files",
|
||||
# type="linelist", default=["**/test_*.txt",
|
||||
@@ -38,6 +39,8 @@ def pytest_addoption(parser):
|
||||
help="exit after first num failures or errors.")
|
||||
group._addoption('--strict', action="store_true",
|
||||
help="run pytest in strict mode, warnings become errors.")
|
||||
group._addoption("-c", metavar="file", type=str, dest="inifilename",
|
||||
help="load configuration from `file` instead of trying to locate one of the implicit configuration files.")
|
||||
|
||||
group = parser.getgroup("collect", "collection")
|
||||
group.addoption('--collectonly', '--collect-only', action="store_true",
|
||||
@@ -63,7 +66,7 @@ def pytest_namespace():
|
||||
return dict(collect=collect)
|
||||
|
||||
def pytest_configure(config):
|
||||
py.test.config = config # compatibiltiy
|
||||
pytest.config = config # compatibiltiy
|
||||
if config.option.exitfirst:
|
||||
config.option.maxfail = 1
|
||||
|
||||
@@ -80,8 +83,9 @@ def wrap_session(config, doit):
|
||||
initstate = 2
|
||||
doit(config, session)
|
||||
except pytest.UsageError:
|
||||
msg = sys.exc_info()[1].args[0]
|
||||
sys.stderr.write("ERROR: %s\n" %(msg,))
|
||||
args = sys.exc_info()[1].args
|
||||
for msg in args:
|
||||
sys.stderr.write("ERROR: %s\n" %(msg,))
|
||||
session.exitstatus = EXIT_USAGEERROR
|
||||
except KeyboardInterrupt:
|
||||
excinfo = py.code.ExceptionInfo()
|
||||
@@ -97,6 +101,7 @@ def wrap_session(config, doit):
|
||||
if session._testsfailed:
|
||||
session.exitstatus = EXIT_TESTSFAILED
|
||||
finally:
|
||||
excinfo = None # Explicitly break reference cycle.
|
||||
session.startdir.chdir()
|
||||
if initstate >= 2:
|
||||
config.hook.pytest_sessionfinish(
|
||||
@@ -143,23 +148,22 @@ def pytest_ignore_collect(path, config):
|
||||
p = path.dirpath()
|
||||
ignore_paths = config._getconftest_pathlist("collect_ignore", path=p)
|
||||
ignore_paths = ignore_paths or []
|
||||
excludeopt = config.getvalue("ignore")
|
||||
excludeopt = config.getoption("ignore")
|
||||
if excludeopt:
|
||||
ignore_paths.extend([py.path.local(x) for x in excludeopt])
|
||||
return path in ignore_paths
|
||||
|
||||
class HookProxy:
|
||||
class FSHookProxy(object):
|
||||
def __init__(self, fspath, config):
|
||||
self.fspath = fspath
|
||||
self.config = config
|
||||
|
||||
def __getattr__(self, name):
|
||||
hookmethod = getattr(self.config.hook, name)
|
||||
plugins = self.config._getmatchingplugins(self.fspath)
|
||||
x = self.config.hook._getcaller(name, plugins)
|
||||
self.__dict__[name] = x
|
||||
return x
|
||||
|
||||
def call_matching_hooks(**kwargs):
|
||||
plugins = self.config._getmatchingplugins(self.fspath)
|
||||
return hookmethod.pcall(plugins, **kwargs)
|
||||
return call_matching_hooks
|
||||
|
||||
def compatproperty(name):
|
||||
def fget(self):
|
||||
@@ -230,6 +234,9 @@ class Node(object):
|
||||
#: allow adding of extra keywords to use for matching
|
||||
self.extra_keyword_matches = set()
|
||||
|
||||
# used for storing artificial fixturedefs for direct parametrization
|
||||
self._name2pseudofixturedef = {}
|
||||
|
||||
#self.extrainit()
|
||||
|
||||
@property
|
||||
@@ -260,6 +267,20 @@ class Node(object):
|
||||
return "<%s %r>" %(self.__class__.__name__,
|
||||
getattr(self, 'name', None))
|
||||
|
||||
def warn(self, code, message):
|
||||
""" generate a warning with the given code and message for this
|
||||
item. """
|
||||
assert isinstance(code, str)
|
||||
fslocation = getattr(self, "location", None)
|
||||
if fslocation is None:
|
||||
fslocation = getattr(self, "fspath", None)
|
||||
else:
|
||||
fslocation = "%s:%s" % fslocation[:2]
|
||||
|
||||
self.ihook.pytest_logwarning(code=code, message=message,
|
||||
nodeid=self.nodeid,
|
||||
fslocation=fslocation)
|
||||
|
||||
# methods for ordering nodes
|
||||
@property
|
||||
def nodeid(self):
|
||||
@@ -270,21 +291,11 @@ class Node(object):
|
||||
self._nodeid = x = self._makeid()
|
||||
return x
|
||||
|
||||
|
||||
def _makeid(self):
|
||||
return self.parent.nodeid + "::" + self.name
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Node):
|
||||
return False
|
||||
return (self.__class__ == other.__class__ and
|
||||
self.name == other.name and self.parent == other.parent)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.parent))
|
||||
return hash(self.nodeid)
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
@@ -304,7 +315,7 @@ class Node(object):
|
||||
except py.builtin._sysex:
|
||||
raise
|
||||
except:
|
||||
failure = py.std.sys.exc_info()
|
||||
failure = sys.exc_info()
|
||||
setattr(self, exattrname, failure)
|
||||
raise
|
||||
setattr(self, attrname, res)
|
||||
@@ -365,6 +376,8 @@ class Node(object):
|
||||
self.session._setupstate.addfinalizer(fin, self)
|
||||
|
||||
def getparent(self, cls):
|
||||
""" get the next parent node (including ourself)
|
||||
which is an instance of the given class"""
|
||||
current = self
|
||||
while current and not isinstance(current, cls):
|
||||
current = current.parent
|
||||
@@ -377,20 +390,24 @@ class Node(object):
|
||||
fm = self.session._fixturemanager
|
||||
if excinfo.errisinstance(fm.FixtureLookupError):
|
||||
return excinfo.value.formatrepr()
|
||||
tbfilter = True
|
||||
if self.config.option.fulltrace:
|
||||
style="long"
|
||||
else:
|
||||
self._prunetraceback(excinfo)
|
||||
# XXX should excinfo.getrepr record all data and toterminal()
|
||||
# process it?
|
||||
tbfilter = False # prunetraceback already does it
|
||||
if style == "auto":
|
||||
style = "long"
|
||||
# XXX should excinfo.getrepr record all data and toterminal() process it?
|
||||
if style is None:
|
||||
if self.config.option.tbstyle == "short":
|
||||
style = "short"
|
||||
else:
|
||||
style = "long"
|
||||
|
||||
return excinfo.getrepr(funcargs=True,
|
||||
showlocals=self.config.option.showlocals,
|
||||
style=style)
|
||||
style=style, tbfilter=tbfilter)
|
||||
|
||||
repr_failure = _repr_failure_py
|
||||
|
||||
@@ -399,10 +416,6 @@ class Collector(Node):
|
||||
and thus iteratively build a tree.
|
||||
"""
|
||||
|
||||
# the set of exceptions to interpret as "Skip the whole module" during
|
||||
# collection
|
||||
skip_exceptions = (Skipped,)
|
||||
|
||||
class CollectError(Exception):
|
||||
""" an error during collection, contains a custom message. """
|
||||
|
||||
@@ -425,7 +438,6 @@ class Collector(Node):
|
||||
|
||||
def _prunetraceback(self, excinfo):
|
||||
if hasattr(self, 'fspath'):
|
||||
path = self.fspath
|
||||
traceback = excinfo.traceback
|
||||
ntraceback = traceback.cut(path=self.fspath)
|
||||
if ntraceback == traceback:
|
||||
@@ -445,9 +457,7 @@ class FSCollector(Collector):
|
||||
self.fspath = fspath
|
||||
|
||||
def _makeid(self):
|
||||
if self == self.session:
|
||||
return "."
|
||||
relpath = self.session.fspath.bestrelpath(self.fspath)
|
||||
relpath = self.fspath.relto(self.config.rootdir)
|
||||
if os.sep != "/":
|
||||
relpath = relpath.replace(os.sep, "/")
|
||||
return relpath
|
||||
@@ -461,6 +471,14 @@ class Item(Node):
|
||||
"""
|
||||
nextitem = None
|
||||
|
||||
def __init__(self, name, parent=None, config=None, session=None):
|
||||
super(Item, self).__init__(name, parent, config, session)
|
||||
self._report_sections = []
|
||||
|
||||
def add_report_section(self, when, key, content):
|
||||
if content:
|
||||
self._report_sections.append((when, key, content))
|
||||
|
||||
def reportinfo(self):
|
||||
return self.fspath, None, ""
|
||||
|
||||
@@ -490,7 +508,7 @@ class Session(FSCollector):
|
||||
__module__ = 'builtins' # for py3
|
||||
|
||||
def __init__(self, config):
|
||||
FSCollector.__init__(self, py.path.local(), parent=None,
|
||||
FSCollector.__init__(self, config.rootdir, parent=None,
|
||||
config=config, session=self)
|
||||
self.config.pluginmanager.register(self, name="session", prepend=True)
|
||||
self._testsfailed = 0
|
||||
@@ -498,6 +516,10 @@ class Session(FSCollector):
|
||||
self.trace = config.trace.root.get("collection")
|
||||
self._norecursepatterns = config.getini("norecursedirs")
|
||||
self.startdir = py.path.local()
|
||||
self._fs2hookproxy = {}
|
||||
|
||||
def _makeid(self):
|
||||
return ""
|
||||
|
||||
def pytest_collectstart(self):
|
||||
if self.shouldstop:
|
||||
@@ -516,7 +538,11 @@ class Session(FSCollector):
|
||||
return path in self._initialpaths
|
||||
|
||||
def gethookproxy(self, fspath):
|
||||
return HookProxy(fspath, self.config)
|
||||
try:
|
||||
return self._fs2hookproxy[fspath]
|
||||
except KeyError:
|
||||
self._fs2hookproxy[fspath] = x = FSHookProxy(fspath, self.config)
|
||||
return x
|
||||
|
||||
def perform_collect(self, args=None, genitems=True):
|
||||
hook = self.config.hook
|
||||
@@ -545,9 +571,12 @@ class Session(FSCollector):
|
||||
self.ihook.pytest_collectreport(report=rep)
|
||||
self.trace.root.indent -= 1
|
||||
if self._notfound:
|
||||
errors = []
|
||||
for arg, exc in self._notfound:
|
||||
line = "(no name %r in any of %r)" % (arg, exc.args[0])
|
||||
raise pytest.UsageError("not found: %s\n%s" %(arg, line))
|
||||
errors.append("not found: %s\n%s" % (arg, line))
|
||||
#XXX: test this
|
||||
raise pytest.UsageError(*errors)
|
||||
if not genitems:
|
||||
return rep.result
|
||||
else:
|
||||
@@ -568,8 +597,7 @@ class Session(FSCollector):
|
||||
# we are inside a make_report hook so
|
||||
# we cannot directly pass through the exception
|
||||
self._notfound.append((arg, sys.exc_info()[1]))
|
||||
self.trace.root.indent -= 1
|
||||
break
|
||||
|
||||
self.trace.root.indent -= 1
|
||||
|
||||
def _collect(self, arg):
|
||||
@@ -636,7 +664,7 @@ class Session(FSCollector):
|
||||
arg = self._tryconvertpyarg(arg)
|
||||
parts = str(arg).split("::")
|
||||
relpath = parts[0].replace("/", os.sep)
|
||||
path = self.fspath.join(relpath, abs=True)
|
||||
path = self.config.invocation_dir.join(relpath, abs=True)
|
||||
if not path.check():
|
||||
if self.config.option.pyargs:
|
||||
msg = "file or package not found: "
|
||||
@@ -698,11 +726,4 @@ class Session(FSCollector):
|
||||
yield x
|
||||
node.ihook.pytest_collectreport(report=rep)
|
||||
|
||||
def getfslineno(obj):
|
||||
# xxx let decorators etc specify a sane ordering
|
||||
if hasattr(obj, 'place_as'):
|
||||
obj = obj.place_as
|
||||
fslineno = py.code.getfslineno(obj)
|
||||
assert isinstance(fslineno[1], int), obj
|
||||
return fslineno
|
||||
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
import py
|
||||
|
||||
|
||||
class MarkerError(Exception):
|
||||
|
||||
"""Error in use of a pytest marker/attribute."""
|
||||
|
||||
|
||||
def pytest_namespace():
|
||||
return {'mark': MarkGenerator()}
|
||||
|
||||
@@ -56,6 +61,11 @@ def pytest_collection_modifyitems(items, config):
|
||||
matchexpr = config.option.markexpr
|
||||
if not keywordexpr and not matchexpr:
|
||||
return
|
||||
# pytest used to allow "-" for negating
|
||||
# but today we just allow "-" at the beginning, use "not" instead
|
||||
# we probably remove "-" alltogether soon
|
||||
if keywordexpr.startswith("-"):
|
||||
keywordexpr = "not " + keywordexpr[1:]
|
||||
selectuntil = False
|
||||
if keywordexpr[-1:] == ":":
|
||||
selectuntil = True
|
||||
@@ -122,7 +132,6 @@ def matchkeyword(colitem, keywordexpr):
|
||||
Additionally, matches on names in the 'extra_keyword_matches' set of
|
||||
any item, as well as names directly assigned to test functions.
|
||||
"""
|
||||
keywordexpr = keywordexpr.replace("-", "not ")
|
||||
mapped_names = set()
|
||||
|
||||
# Add the names of the current item and any parent items
|
||||
@@ -140,7 +149,13 @@ def matchkeyword(colitem, keywordexpr):
|
||||
for name in colitem.function.__dict__:
|
||||
mapped_names.add(name)
|
||||
|
||||
return eval(keywordexpr, {}, KeywordMapping(mapped_names))
|
||||
mapping = KeywordMapping(mapped_names)
|
||||
if " " not in keywordexpr:
|
||||
# special case to allow for simple "-k pass" and "-k 1.3"
|
||||
return mapping[keywordexpr]
|
||||
elif keywordexpr.startswith("not ") and " " not in keywordexpr[4:]:
|
||||
return not mapping[keywordexpr[4:]]
|
||||
return eval(keywordexpr, {}, mapping)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
@@ -151,10 +166,10 @@ def pytest_configure(config):
|
||||
|
||||
class MarkGenerator:
|
||||
""" Factory for :class:`MarkDecorator` objects - exposed as
|
||||
a ``py.test.mark`` singleton instance. Example::
|
||||
a ``pytest.mark`` singleton instance. Example::
|
||||
|
||||
import py
|
||||
@py.test.mark.slowtest
|
||||
@pytest.mark.slowtest
|
||||
def test_function():
|
||||
pass
|
||||
|
||||
@@ -163,7 +178,7 @@ class MarkGenerator:
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name[0] == "_":
|
||||
raise AttributeError(name)
|
||||
raise AttributeError("Marker name must NOT start with underscore")
|
||||
if hasattr(self, '_config'):
|
||||
self._check(name)
|
||||
return MarkDecorator(name)
|
||||
@@ -182,6 +197,9 @@ class MarkGenerator:
|
||||
if name not in self._markers:
|
||||
raise AttributeError("%r not a registered marker" % (name,))
|
||||
|
||||
def istestfunc(func):
|
||||
return hasattr(func, "__call__") and \
|
||||
getattr(func, "__name__", "<lambda>") != "<lambda>"
|
||||
|
||||
class MarkDecorator:
|
||||
""" A decorator for test functions and test classes. When applied
|
||||
@@ -189,14 +207,32 @@ class MarkDecorator:
|
||||
:ref:`retrieved by hooks as item keywords <excontrolskip>`.
|
||||
MarkDecorator instances are often created like this::
|
||||
|
||||
mark1 = py.test.mark.NAME # simple MarkDecorator
|
||||
mark2 = py.test.mark.NAME(name1=value) # parametrized MarkDecorator
|
||||
mark1 = pytest.mark.NAME # simple MarkDecorator
|
||||
mark2 = pytest.mark.NAME(name1=value) # parametrized MarkDecorator
|
||||
|
||||
and can then be applied as decorators to test functions::
|
||||
|
||||
@mark2
|
||||
def test_function():
|
||||
pass
|
||||
|
||||
When a MarkDecorator instance is called it does the following:
|
||||
1. If called with a single class as its only positional argument and no
|
||||
additional keyword arguments, it attaches itself to the class so it
|
||||
gets applied automatically to all test cases found in that class.
|
||||
2. If called with a single function as its only positional argument and
|
||||
no additional keyword arguments, it attaches a MarkInfo object to the
|
||||
function, containing all the arguments already stored internally in
|
||||
the MarkDecorator.
|
||||
3. When called in any other case, it performs a 'fake construction' call,
|
||||
i.e. it returns a new MarkDecorator instance with the original
|
||||
MarkDecorator's content updated with the arguments passed to this
|
||||
call.
|
||||
|
||||
Note: The rules above prevent MarkDecorator objects from storing only a
|
||||
single function or class reference as their positional argument with no
|
||||
additional keyword or positional arguments.
|
||||
|
||||
"""
|
||||
def __init__(self, name, args=None, kwargs=None):
|
||||
self.name = name
|
||||
@@ -215,10 +251,10 @@ class MarkDecorator:
|
||||
def __call__(self, *args, **kwargs):
|
||||
""" if passed a single callable argument: decorate it with mark info.
|
||||
otherwise add *args/**kwargs in-place to mark information. """
|
||||
if args:
|
||||
if args and not kwargs:
|
||||
func = args[0]
|
||||
if len(args) == 1 and hasattr(func, '__call__') or \
|
||||
hasattr(func, '__bases__'):
|
||||
if len(args) == 1 and (istestfunc(func) or
|
||||
hasattr(func, '__bases__')):
|
||||
if hasattr(func, '__bases__'):
|
||||
if hasattr(func, 'pytestmark'):
|
||||
l = func.pytestmark
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
""" monkeypatching and mocking functionality. """
|
||||
|
||||
import os, sys
|
||||
from py.builtin import _basestring
|
||||
|
||||
def pytest_funcarg__monkeypatch(request):
|
||||
"""The returned ``monkeypatch`` funcarg provides these
|
||||
@@ -28,7 +29,7 @@ def pytest_funcarg__monkeypatch(request):
|
||||
|
||||
def derive_importpath(import_path):
|
||||
import pytest
|
||||
if not isinstance(import_path, str) or "." not in import_path:
|
||||
if not isinstance(import_path, _basestring) or "." not in import_path:
|
||||
raise TypeError("must be absolute import path string, not %r" %
|
||||
(import_path,))
|
||||
rest = []
|
||||
@@ -58,17 +59,21 @@ def derive_importpath(import_path):
|
||||
|
||||
|
||||
|
||||
notset = object()
|
||||
class Notset:
|
||||
def __repr__(self):
|
||||
return "<notset>"
|
||||
|
||||
notset = Notset()
|
||||
|
||||
class monkeypatch:
|
||||
""" object keeping a record of setattr/item/env/syspath changes. """
|
||||
""" Object keeping a record of setattr/item/env/syspath changes. """
|
||||
def __init__(self):
|
||||
self._setattr = []
|
||||
self._setitem = []
|
||||
self._cwd = None
|
||||
|
||||
def setattr(self, target, name, value=notset, raising=True):
|
||||
""" set attribute value on target, memorizing the old value.
|
||||
""" Set attribute value on target, memorizing the old value.
|
||||
By default raise AttributeError if the attribute did not exist.
|
||||
|
||||
For convenience you can specify a string as ``target`` which
|
||||
@@ -85,7 +90,7 @@ class monkeypatch:
|
||||
import inspect
|
||||
|
||||
if value is notset:
|
||||
if not isinstance(target, str):
|
||||
if not isinstance(target, _basestring):
|
||||
raise TypeError("use setattr(target, name, value) or "
|
||||
"setattr(target, value) with target being a dotted "
|
||||
"import string")
|
||||
@@ -103,19 +108,19 @@ class monkeypatch:
|
||||
setattr(target, name, value)
|
||||
|
||||
def delattr(self, target, name=notset, raising=True):
|
||||
""" delete attribute ``name`` from ``target``, by default raise
|
||||
""" Delete attribute ``name`` from ``target``, by default raise
|
||||
AttributeError it the attribute did not previously exist.
|
||||
|
||||
If no ``name`` is specified and ``target`` is a string
|
||||
it will be interpreted as a dotted import path with the
|
||||
last part being the attribute name.
|
||||
|
||||
If raising is set to false, the attribute is allowed to not
|
||||
pre-exist.
|
||||
If ``raising`` is set to False, no exception will be raised if the
|
||||
attribute is missing.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
if name is notset:
|
||||
if not isinstance(target, str):
|
||||
if not isinstance(target, _basestring):
|
||||
raise TypeError("use delattr(target, name) or "
|
||||
"delattr(target) with target being a dotted "
|
||||
"import string")
|
||||
@@ -130,12 +135,16 @@ class monkeypatch:
|
||||
delattr(target, name)
|
||||
|
||||
def setitem(self, dic, name, value):
|
||||
""" set dictionary entry ``name`` to value. """
|
||||
""" Set dictionary entry ``name`` to value. """
|
||||
self._setitem.insert(0, (dic, name, dic.get(name, notset)))
|
||||
dic[name] = value
|
||||
|
||||
def delitem(self, dic, name, raising=True):
|
||||
""" delete ``name`` from dict, raise KeyError if it doesn't exist."""
|
||||
""" Delete ``name`` from dict. Raise KeyError if it doesn't exist.
|
||||
|
||||
If ``raising`` is set to False, no exception will be raised if the
|
||||
key is missing.
|
||||
"""
|
||||
if name not in dic:
|
||||
if raising:
|
||||
raise KeyError(name)
|
||||
@@ -144,7 +153,7 @@ class monkeypatch:
|
||||
del dic[name]
|
||||
|
||||
def setenv(self, name, value, prepend=None):
|
||||
""" set environment variable ``name`` to ``value``. if ``prepend``
|
||||
""" Set environment variable ``name`` to ``value``. If ``prepend``
|
||||
is a character, read the current environment variable value
|
||||
and prepend the ``value`` adjoined with the ``prepend`` character."""
|
||||
value = str(value)
|
||||
@@ -153,17 +162,22 @@ class monkeypatch:
|
||||
self.setitem(os.environ, name, value)
|
||||
|
||||
def delenv(self, name, raising=True):
|
||||
""" delete ``name`` from environment, raise KeyError it not exists."""
|
||||
""" Delete ``name`` from the environment. Raise KeyError it does not
|
||||
exist.
|
||||
|
||||
If ``raising`` is set to False, no exception will be raised if the
|
||||
environment variable is missing.
|
||||
"""
|
||||
self.delitem(os.environ, name, raising=raising)
|
||||
|
||||
def syspath_prepend(self, path):
|
||||
""" prepend ``path`` to ``sys.path`` list of import locations. """
|
||||
""" Prepend ``path`` to ``sys.path`` list of import locations. """
|
||||
if not hasattr(self, '_savesyspath'):
|
||||
self._savesyspath = sys.path[:]
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
def chdir(self, path):
|
||||
""" change the current working directory to the specified path
|
||||
""" Change the current working directory to the specified path
|
||||
path can be a string or a py.path.local object
|
||||
"""
|
||||
if self._cwd is None:
|
||||
@@ -174,9 +188,9 @@ class monkeypatch:
|
||||
os.chdir(path)
|
||||
|
||||
def undo(self):
|
||||
""" undo previous changes. This call consumes the
|
||||
undo stack. Calling it a second time has no effect unless
|
||||
you do more monkeypatching after the undo call."""
|
||||
""" Undo previous changes. This call consumes the
|
||||
undo stack. Calling it a second time has no effect unless
|
||||
you do more monkeypatching after the undo call."""
|
||||
for obj, name, value in self._setattr:
|
||||
if value is not notset:
|
||||
setattr(obj, name, value)
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
""" run test suites written for nose. """
|
||||
|
||||
import pytest, py
|
||||
import sys
|
||||
|
||||
import py
|
||||
import pytest
|
||||
from _pytest import unittest
|
||||
|
||||
def pytest_runtest_makereport(__multicall__, item, call):
|
||||
SkipTest = getattr(sys.modules.get('nose', None), 'SkipTest', None)
|
||||
if SkipTest:
|
||||
if call.excinfo and call.excinfo.errisinstance(SkipTest):
|
||||
# let's substitute the excinfo with a py.test.skip one
|
||||
call2 = call.__class__(lambda:
|
||||
pytest.skip(str(call.excinfo.value)), call.when)
|
||||
call.excinfo = call2.excinfo
|
||||
|
||||
def get_skip_exceptions():
|
||||
skip_classes = set()
|
||||
for module_name in ('unittest', 'unittest2', 'nose'):
|
||||
mod = sys.modules.get(module_name)
|
||||
if hasattr(mod, 'SkipTest'):
|
||||
skip_classes.add(mod.SkipTest)
|
||||
return tuple(skip_classes)
|
||||
|
||||
|
||||
def pytest_runtest_makereport(item, call):
|
||||
if call.excinfo and call.excinfo.errisinstance(get_skip_exceptions()):
|
||||
# let's substitute the excinfo with a pytest.skip one
|
||||
call2 = call.__class__(lambda:
|
||||
pytest.skip(str(call.excinfo.value)), call.when)
|
||||
call.excinfo = call2.excinfo
|
||||
|
||||
|
||||
@pytest.mark.trylast
|
||||
@@ -38,13 +48,8 @@ def teardown_nose(item):
|
||||
# #call_optional(item._nosegensetup, 'teardown')
|
||||
# del item.parent._nosegensetup
|
||||
|
||||
|
||||
def pytest_make_collect_report(collector):
|
||||
SkipTest = getattr(sys.modules.get('unittest', None), 'SkipTest', None)
|
||||
if SkipTest is not None:
|
||||
collector.skip_exceptions += (SkipTest,)
|
||||
SkipTest = getattr(sys.modules.get('nose', None), 'SkipTest', None)
|
||||
if SkipTest is not None:
|
||||
collector.skip_exceptions += (SkipTest,)
|
||||
if isinstance(collector, pytest.Generator):
|
||||
call_optional(collector.obj, 'setup')
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
""" submit failure or test session information to a pastebin service. """
|
||||
import pytest
|
||||
import py, sys
|
||||
import tempfile
|
||||
|
||||
class url:
|
||||
base = "http://bpaste.net"
|
||||
xmlrpc = base + "/xmlrpc/"
|
||||
show = base + "/show/"
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting")
|
||||
@@ -13,36 +11,62 @@ def pytest_addoption(parser):
|
||||
choices=['failed', 'all'],
|
||||
help="send failed|all info to bpaste.net pastebin service.")
|
||||
|
||||
def pytest_configure(__multicall__, config):
|
||||
import tempfile
|
||||
__multicall__.execute()
|
||||
@pytest.mark.trylast
|
||||
def pytest_configure(config):
|
||||
if config.option.pastebin == "all":
|
||||
config._pastebinfile = tempfile.TemporaryFile('w+')
|
||||
tr = config.pluginmanager.getplugin('terminalreporter')
|
||||
oldwrite = tr._tw.write
|
||||
def tee_write(s, **kwargs):
|
||||
oldwrite(s, **kwargs)
|
||||
config._pastebinfile.write(str(s))
|
||||
tr._tw.write = tee_write
|
||||
# if no terminal reporter plugin is present, nothing we can do here;
|
||||
# this can happen when this function executes in a slave node
|
||||
# when using pytest-xdist, for example
|
||||
if tr is not None:
|
||||
config._pastebinfile = tempfile.TemporaryFile('w+')
|
||||
oldwrite = tr._tw.write
|
||||
def tee_write(s, **kwargs):
|
||||
oldwrite(s, **kwargs)
|
||||
config._pastebinfile.write(str(s))
|
||||
tr._tw.write = tee_write
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
if hasattr(config, '_pastebinfile'):
|
||||
# get terminal contents and delete file
|
||||
config._pastebinfile.seek(0)
|
||||
sessionlog = config._pastebinfile.read()
|
||||
config._pastebinfile.close()
|
||||
del config._pastebinfile
|
||||
proxyid = getproxy().newPaste("python", sessionlog)
|
||||
pastebinurl = "%s%s" % (url.show, proxyid)
|
||||
sys.stderr.write("pastebin session-log: %s\n" % pastebinurl)
|
||||
# undo our patching in the terminal reporter
|
||||
tr = config.pluginmanager.getplugin('terminalreporter')
|
||||
del tr._tw.__dict__['write']
|
||||
# write summary
|
||||
tr.write_sep("=", "Sending information to Paste Service")
|
||||
pastebinurl = create_new_paste(sessionlog)
|
||||
tr.write_line("pastebin session-log: %s\n" % pastebinurl)
|
||||
|
||||
def getproxy():
|
||||
def create_new_paste(contents):
|
||||
"""
|
||||
Creates a new paste using bpaste.net service.
|
||||
|
||||
:contents: paste contents
|
||||
:returns: url to the pasted contents
|
||||
"""
|
||||
import re
|
||||
if sys.version_info < (3, 0):
|
||||
from xmlrpclib import ServerProxy
|
||||
from urllib import urlopen, urlencode
|
||||
else:
|
||||
from xmlrpc.client import ServerProxy
|
||||
return ServerProxy(url.xmlrpc).pastes
|
||||
from urllib.request import urlopen
|
||||
from urllib.parse import urlencode
|
||||
|
||||
params = {
|
||||
'code': contents,
|
||||
'lexer': 'python3' if sys.version_info[0] == 3 else 'python',
|
||||
'expiry': '1week',
|
||||
}
|
||||
url = 'https://bpaste.net'
|
||||
response = urlopen(url, data=urlencode(params)).read()
|
||||
m = re.search(r'href="/raw/(\w+)"', response)
|
||||
if m:
|
||||
return '%s/show/%s' % (url, m.group(1))
|
||||
else:
|
||||
return 'bad response: ' + response
|
||||
|
||||
def pytest_terminal_summary(terminalreporter):
|
||||
if terminalreporter.config.option.pastebin != "failed":
|
||||
@@ -50,9 +74,6 @@ def pytest_terminal_summary(terminalreporter):
|
||||
tr = terminalreporter
|
||||
if 'failed' in tr.stats:
|
||||
terminalreporter.write_sep("=", "Sending information to Paste Service")
|
||||
if tr.config.option.debug:
|
||||
terminalreporter.write_line("xmlrpcurl: %s" %(url.xmlrpc,))
|
||||
serverproxy = getproxy()
|
||||
for rep in terminalreporter.stats.get('failed'):
|
||||
try:
|
||||
msg = rep.longrepr.reprtraceback.reprentries[-1].reprfileloc
|
||||
@@ -62,6 +83,5 @@ def pytest_terminal_summary(terminalreporter):
|
||||
rep.toterminal(tw)
|
||||
s = tw.stringio.getvalue()
|
||||
assert len(s)
|
||||
proxyid = serverproxy.newPaste("python", s)
|
||||
pastebinurl = "%s%s" % (url.show, proxyid)
|
||||
pastebinurl = create_new_paste(s)
|
||||
tr.write_line("%s --> %s" %(msg, pastebinurl))
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
""" interactive debugging with PDB, the Python Debugger. """
|
||||
|
||||
import pytest, py
|
||||
from __future__ import absolute_import
|
||||
import pdb
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import py
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general")
|
||||
group._addoption('--pdb',
|
||||
@@ -16,50 +20,38 @@ def pytest_configure(config):
|
||||
if config.getvalue("usepdb"):
|
||||
config.pluginmanager.register(PdbInvoke(), 'pdbinvoke')
|
||||
|
||||
old_trace = py.std.pdb.set_trace
|
||||
old = (pdb.set_trace, pytestPDB._pluginmanager)
|
||||
def fin():
|
||||
py.std.pdb.set_trace = old_trace
|
||||
py.std.pdb.set_trace = pytest.set_trace
|
||||
pdb.set_trace, pytestPDB._pluginmanager = old
|
||||
pdb.set_trace = pytest.set_trace
|
||||
pytestPDB._pluginmanager = config.pluginmanager
|
||||
config._cleanup.append(fin)
|
||||
|
||||
class pytestPDB:
|
||||
""" Pseudo PDB that defers to the real pdb. """
|
||||
item = None
|
||||
collector = None
|
||||
_pluginmanager = None
|
||||
|
||||
def set_trace(self):
|
||||
""" invoke PDB set_trace debugging, dropping any IO capturing. """
|
||||
frame = sys._getframe().f_back
|
||||
item = self.item or self.collector
|
||||
|
||||
if item is not None:
|
||||
capman = item.config.pluginmanager.getplugin("capturemanager")
|
||||
out, err = capman.suspendcapture()
|
||||
if hasattr(item, 'outerr'):
|
||||
item.outerr = (item.outerr[0] + out, item.outerr[1] + err)
|
||||
capman = None
|
||||
if self._pluginmanager is not None:
|
||||
capman = self._pluginmanager.getplugin("capturemanager")
|
||||
if capman:
|
||||
capman.suspendcapture(in_=True)
|
||||
tw = py.io.TerminalWriter()
|
||||
tw.line()
|
||||
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
|
||||
py.std.pdb.Pdb().set_trace(frame)
|
||||
self._pluginmanager.hook.pytest_enter_pdb()
|
||||
pdb.Pdb().set_trace(frame)
|
||||
|
||||
def pdbitem(item):
|
||||
pytestPDB.item = item
|
||||
pytest_runtest_setup = pytest_runtest_call = pytest_runtest_teardown = pdbitem
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_make_collect_report(__multicall__, collector):
|
||||
try:
|
||||
pytestPDB.collector = collector
|
||||
return __multicall__.execute()
|
||||
finally:
|
||||
pytestPDB.collector = None
|
||||
|
||||
def pytest_runtest_makereport():
|
||||
pytestPDB.item = None
|
||||
|
||||
class PdbInvoke:
|
||||
def pytest_exception_interact(self, node, call, report):
|
||||
return _enter_pdb(node, call.excinfo, report)
|
||||
capman = node.config.pluginmanager.getplugin("capturemanager")
|
||||
if capman:
|
||||
capman.suspendcapture(in_=True)
|
||||
_enter_pdb(node, call.excinfo, report)
|
||||
|
||||
def pytest_internalerror(self, excrepr, excinfo):
|
||||
for line in str(excrepr).split("\n"):
|
||||
@@ -87,7 +79,8 @@ def _enter_pdb(node, excinfo, rep):
|
||||
def _postmortem_traceback(excinfo):
|
||||
# A doctest.UnexpectedException is not useful for post_mortem.
|
||||
# Use the underlying exception instead:
|
||||
if isinstance(excinfo.value, py.std.doctest.UnexpectedException):
|
||||
from doctest import UnexpectedException
|
||||
if isinstance(excinfo.value, UnexpectedException):
|
||||
return excinfo.value.exc_info[2]
|
||||
else:
|
||||
return excinfo._excinfo[2]
|
||||
@@ -101,7 +94,6 @@ def _find_last_non_hidden_frame(stack):
|
||||
|
||||
|
||||
def post_mortem(t):
|
||||
pdb = py.std.pdb
|
||||
class Pdb(pdb.Pdb):
|
||||
def get_stack(self, f, t):
|
||||
stack, i = pdb.Pdb.get_stack(self, f, t)
|
||||
|
||||
@@ -1,14 +1,37 @@
|
||||
""" (disabled by default) support for testing py.test and py.test plugins. """
|
||||
|
||||
import py, pytest
|
||||
import sys, os
|
||||
""" (disabled by default) support for testing pytest and pytest plugins. """
|
||||
import sys
|
||||
import os
|
||||
import codecs
|
||||
import re
|
||||
import time
|
||||
import platform
|
||||
from fnmatch import fnmatch
|
||||
from _pytest.main import Session, EXIT_OK
|
||||
import subprocess
|
||||
|
||||
import py
|
||||
import pytest
|
||||
from py.builtin import print_
|
||||
from _pytest.core import HookRelay
|
||||
from _pytest.core import HookCaller, add_method_wrapper
|
||||
|
||||
from _pytest.main import Session, EXIT_OK
|
||||
|
||||
# used at least by pytest-xdist plugin
|
||||
@pytest.fixture
|
||||
def _pytest(request):
|
||||
""" Return a helper which offers a gethookrecorder(hook)
|
||||
method which returns a HookRecorder instance which helps
|
||||
to make assertions about called hooks.
|
||||
"""
|
||||
return PytestArg(request)
|
||||
|
||||
class PytestArg:
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
|
||||
def gethookrecorder(self, hook):
|
||||
hookrecorder = HookRecorder(hook._pm)
|
||||
self.request.addfinalizer(hookrecorder.finish_recording)
|
||||
return hookrecorder
|
||||
|
||||
|
||||
def get_public_names(l):
|
||||
@@ -26,31 +49,16 @@ def pytest_addoption(parser):
|
||||
def pytest_configure(config):
|
||||
# This might be called multiple times. Only take the first.
|
||||
global _pytest_fullpath
|
||||
import pytest
|
||||
try:
|
||||
_pytest_fullpath
|
||||
except NameError:
|
||||
_pytest_fullpath = os.path.abspath(pytest.__file__.rstrip("oc"))
|
||||
_pytest_fullpath = _pytest_fullpath.replace("$py.class", ".py")
|
||||
|
||||
def pytest_funcarg___pytest(request):
|
||||
return PytestArg(request)
|
||||
|
||||
class PytestArg:
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
|
||||
def gethookrecorder(self, hook):
|
||||
hookrecorder = HookRecorder(hook._pm)
|
||||
hookrecorder.start_recording(hook._hookspecs)
|
||||
self.request.addfinalizer(hookrecorder.finish_recording)
|
||||
return hookrecorder
|
||||
|
||||
class ParsedCall:
|
||||
def __init__(self, name, locals):
|
||||
assert '_name' not in locals
|
||||
self.__dict__.update(locals)
|
||||
self.__dict__.pop('self')
|
||||
def __init__(self, name, kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
self._name = name
|
||||
|
||||
def __repr__(self):
|
||||
@@ -58,73 +66,31 @@ class ParsedCall:
|
||||
del d['_name']
|
||||
return "<ParsedCall %r(**%r)>" %(self._name, d)
|
||||
|
||||
|
||||
class HookRecorder:
|
||||
def __init__(self, pluginmanager):
|
||||
self._pluginmanager = pluginmanager
|
||||
self.calls = []
|
||||
self._recorders = {}
|
||||
|
||||
def start_recording(self, hookspecs):
|
||||
if not isinstance(hookspecs, (list, tuple)):
|
||||
hookspecs = [hookspecs]
|
||||
for hookspec in hookspecs:
|
||||
assert hookspec not in self._recorders
|
||||
class RecordCalls:
|
||||
_recorder = self
|
||||
for name, method in vars(hookspec).items():
|
||||
if name[0] != "_":
|
||||
setattr(RecordCalls, name, self._makecallparser(method))
|
||||
recorder = RecordCalls()
|
||||
self._recorders[hookspec] = recorder
|
||||
self._pluginmanager.register(recorder)
|
||||
self.hook = HookRelay(hookspecs, pm=self._pluginmanager,
|
||||
prefix="pytest_")
|
||||
def _docall(hookcaller, methods, kwargs):
|
||||
self.calls.append(ParsedCall(hookcaller.name, kwargs))
|
||||
yield
|
||||
self._undo_wrapping = add_method_wrapper(HookCaller, _docall)
|
||||
pluginmanager.add_shutdown(self._undo_wrapping)
|
||||
|
||||
def finish_recording(self):
|
||||
for recorder in self._recorders.values():
|
||||
if self._pluginmanager.isregistered(recorder):
|
||||
self._pluginmanager.unregister(recorder)
|
||||
self._recorders.clear()
|
||||
|
||||
def _makecallparser(self, method):
|
||||
name = method.__name__
|
||||
args, varargs, varkw, default = py.std.inspect.getargspec(method)
|
||||
if not args or args[0] != "self":
|
||||
args.insert(0, 'self')
|
||||
fspec = py.std.inspect.formatargspec(args, varargs, varkw, default)
|
||||
# we use exec because we want to have early type
|
||||
# errors on wrong input arguments, using
|
||||
# *args/**kwargs delays this and gives errors
|
||||
# elsewhere
|
||||
exec (py.code.compile("""
|
||||
def %(name)s%(fspec)s:
|
||||
self._recorder.calls.append(
|
||||
ParsedCall(%(name)r, locals()))
|
||||
""" % locals()))
|
||||
return locals()[name]
|
||||
self._undo_wrapping()
|
||||
|
||||
def getcalls(self, names):
|
||||
if isinstance(names, str):
|
||||
names = names.split()
|
||||
for name in names:
|
||||
for cls in self._recorders:
|
||||
if name in vars(cls):
|
||||
break
|
||||
else:
|
||||
raise ValueError("callname %r not found in %r" %(
|
||||
name, self._recorders.keys()))
|
||||
l = []
|
||||
for call in self.calls:
|
||||
if call._name in names:
|
||||
l.append(call)
|
||||
return l
|
||||
return [call for call in self.calls if call._name in names]
|
||||
|
||||
def contains(self, entries):
|
||||
def assert_contains(self, entries):
|
||||
__tracebackhide__ = True
|
||||
from py.builtin import print_
|
||||
i = 0
|
||||
entries = list(entries)
|
||||
backlocals = py.std.sys._getframe(1).f_locals
|
||||
backlocals = sys._getframe(1).f_locals
|
||||
while entries:
|
||||
name, check = entries.pop(0)
|
||||
for ind, call in enumerate(self.calls[i:]):
|
||||
@@ -139,7 +105,7 @@ class HookRecorder:
|
||||
break
|
||||
print_("NONAMEMATCH", name, "with", call)
|
||||
else:
|
||||
py.test.fail("could not find %r check %r" % (name, check))
|
||||
pytest.fail("could not find %r check %r" % (name, check))
|
||||
|
||||
def popcall(self, name):
|
||||
__tracebackhide__ = True
|
||||
@@ -149,13 +115,76 @@ class HookRecorder:
|
||||
return call
|
||||
lines = ["could not find call %r, in:" % (name,)]
|
||||
lines.extend([" %s" % str(x) for x in self.calls])
|
||||
py.test.fail("\n".join(lines))
|
||||
pytest.fail("\n".join(lines))
|
||||
|
||||
def getcall(self, name):
|
||||
l = self.getcalls(name)
|
||||
assert len(l) == 1, (name, l)
|
||||
return l[0]
|
||||
|
||||
# functionality for test reports
|
||||
|
||||
def getreports(self,
|
||||
names="pytest_runtest_logreport pytest_collectreport"):
|
||||
return [x.report for x in self.getcalls(names)]
|
||||
|
||||
def matchreport(self, inamepart="",
|
||||
names="pytest_runtest_logreport pytest_collectreport", when=None):
|
||||
""" return a testreport whose dotted import path matches """
|
||||
l = []
|
||||
for rep in self.getreports(names=names):
|
||||
try:
|
||||
if not when and rep.when != "call" and rep.passed:
|
||||
# setup/teardown passing reports - let's ignore those
|
||||
continue
|
||||
except AttributeError:
|
||||
pass
|
||||
if when and getattr(rep, 'when', None) != when:
|
||||
continue
|
||||
if not inamepart or inamepart in rep.nodeid.split("::"):
|
||||
l.append(rep)
|
||||
if not l:
|
||||
raise ValueError("could not find test report matching %r: "
|
||||
"no test reports at all!" % (inamepart,))
|
||||
if len(l) > 1:
|
||||
raise ValueError(
|
||||
"found 2 or more testreports matching %r: %s" %(inamepart, l))
|
||||
return l[0]
|
||||
|
||||
def getfailures(self,
|
||||
names='pytest_runtest_logreport pytest_collectreport'):
|
||||
return [rep for rep in self.getreports(names) if rep.failed]
|
||||
|
||||
def getfailedcollections(self):
|
||||
return self.getfailures('pytest_collectreport')
|
||||
|
||||
def listoutcomes(self):
|
||||
passed = []
|
||||
skipped = []
|
||||
failed = []
|
||||
for rep in self.getreports(
|
||||
"pytest_collectreport pytest_runtest_logreport"):
|
||||
if rep.passed:
|
||||
if getattr(rep, "when", None) == "call":
|
||||
passed.append(rep)
|
||||
elif rep.skipped:
|
||||
skipped.append(rep)
|
||||
elif rep.failed:
|
||||
failed.append(rep)
|
||||
return passed, skipped, failed
|
||||
|
||||
def countoutcomes(self):
|
||||
return [len(x) for x in self.listoutcomes()]
|
||||
|
||||
def assertoutcome(self, passed=0, skipped=0, failed=0):
|
||||
realpassed, realskipped, realfailed = self.listoutcomes()
|
||||
assert passed == len(realpassed)
|
||||
assert skipped == len(realskipped)
|
||||
assert failed == len(realfailed)
|
||||
|
||||
def clear(self):
|
||||
self.calls[:] = []
|
||||
|
||||
|
||||
def pytest_funcarg__linecomp(request):
|
||||
return LineComp()
|
||||
@@ -191,7 +220,6 @@ class TmpTestdir:
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
self.Config = request.config.__class__
|
||||
self._pytest = request.getfuncargvalue("_pytest")
|
||||
# XXX remove duplication with tmpdir plugin
|
||||
basetmp = request.config._tmpdirhandler.ensuretemp("testdir")
|
||||
name = request.function.__name__
|
||||
@@ -212,7 +240,7 @@ class TmpTestdir:
|
||||
|
||||
def finalize(self):
|
||||
for p in self._syspathremove:
|
||||
py.std.sys.path.remove(p)
|
||||
sys.path.remove(p)
|
||||
if hasattr(self, '_olddir'):
|
||||
self._olddir.chdir()
|
||||
# delete modules that have been loaded from tmpdir
|
||||
@@ -222,15 +250,10 @@ class TmpTestdir:
|
||||
if fn and fn.startswith(str(self.tmpdir)):
|
||||
del sys.modules[name]
|
||||
|
||||
def getreportrecorder(self, obj):
|
||||
if hasattr(obj, 'config'):
|
||||
obj = obj.config
|
||||
if hasattr(obj, 'hook'):
|
||||
obj = obj.hook
|
||||
assert hasattr(obj, '_hookspecs'), obj
|
||||
reprec = ReportRecorder(obj)
|
||||
reprec.hookrecorder = self._pytest.gethookrecorder(obj)
|
||||
reprec.hook = reprec.hookrecorder.hook
|
||||
def make_hook_recorder(self, pluginmanager):
|
||||
assert not hasattr(pluginmanager, "reprec")
|
||||
pluginmanager.reprec = reprec = HookRecorder(pluginmanager)
|
||||
self.request.addfinalizer(reprec.finish_recording)
|
||||
return reprec
|
||||
|
||||
def chdir(self):
|
||||
@@ -248,8 +271,14 @@ class TmpTestdir:
|
||||
ret = None
|
||||
for name, value in items:
|
||||
p = self.tmpdir.join(name).new(ext=ext)
|
||||
source = py.builtin._totext(py.code.Source(value)).strip()
|
||||
content = source.encode("utf-8") # + "\n"
|
||||
source = py.code.Source(value)
|
||||
def my_totext(s, encoding="utf-8"):
|
||||
if py.builtin._isbytes(s):
|
||||
s = py.builtin._totext(s, encoding=encoding)
|
||||
return s
|
||||
source_unicode = "\n".join([my_totext(line) for line in source.lines])
|
||||
source = py.builtin._totext(source_unicode)
|
||||
content = source.strip().encode("utf-8") # + "\n"
|
||||
#content = content.rstrip() + "\n"
|
||||
p.write(content, "wb")
|
||||
if ret is None:
|
||||
@@ -260,9 +289,6 @@ class TmpTestdir:
|
||||
def makefile(self, ext, *args, **kwargs):
|
||||
return self._makefile(ext, args, kwargs)
|
||||
|
||||
def makeini(self, source):
|
||||
return self.makefile('cfg', setup=source)
|
||||
|
||||
def makeconftest(self, source):
|
||||
return self.makepyfile(conftest=source)
|
||||
|
||||
@@ -282,7 +308,7 @@ class TmpTestdir:
|
||||
def syspathinsert(self, path=None):
|
||||
if path is None:
|
||||
path = self.tmpdir
|
||||
py.std.sys.path.insert(0, str(path))
|
||||
sys.path.insert(0, str(path))
|
||||
self._syspathremove.append(str(path))
|
||||
|
||||
def mkdir(self, name):
|
||||
@@ -298,9 +324,8 @@ class TmpTestdir:
|
||||
session = Session(config)
|
||||
assert '::' not in str(arg)
|
||||
p = py.path.local(arg)
|
||||
x = session.fspath.bestrelpath(p)
|
||||
config.hook.pytest_sessionstart(session=session)
|
||||
res = session.perform_collect([x], genitems=False)[0]
|
||||
res = session.perform_collect([str(p)], genitems=False)[0]
|
||||
config.hook.pytest_sessionfinish(session=session, exitstatus=EXIT_OK)
|
||||
return res
|
||||
|
||||
@@ -346,26 +371,23 @@ class TmpTestdir:
|
||||
def inline_genitems(self, *args):
|
||||
return self.inprocess_run(list(args) + ['--collectonly'])
|
||||
|
||||
def inline_run(self, *args):
|
||||
items, rec = self.inprocess_run(args)
|
||||
return rec
|
||||
def inprocess_run(self, args, plugins=()):
|
||||
rec = self.inline_run(*args, plugins=plugins)
|
||||
items = [x.item for x in rec.getcalls("pytest_itemcollected")]
|
||||
return items, rec
|
||||
|
||||
def inprocess_run(self, args, plugins=None):
|
||||
def inline_run(self, *args, **kwargs):
|
||||
rec = []
|
||||
items = []
|
||||
class Collect:
|
||||
def pytest_configure(x, config):
|
||||
rec.append(self.getreportrecorder(config))
|
||||
def pytest_itemcollected(self, item):
|
||||
items.append(item)
|
||||
if not plugins:
|
||||
plugins = []
|
||||
rec.append(self.make_hook_recorder(config.pluginmanager))
|
||||
plugins = kwargs.get("plugins") or []
|
||||
plugins.append(Collect())
|
||||
ret = pytest.main(list(args), plugins=plugins)
|
||||
assert len(rec) == 1
|
||||
reprec = rec[0]
|
||||
reprec.ret = ret
|
||||
assert len(rec) == 1
|
||||
return items, reprec
|
||||
return reprec
|
||||
|
||||
def parseconfig(self, *args):
|
||||
args = [str(x) for x in args]
|
||||
@@ -390,8 +412,7 @@ class TmpTestdir:
|
||||
def parseconfigure(self, *args):
|
||||
config = self.parseconfig(*args)
|
||||
config.do_configure()
|
||||
self.request.addfinalizer(lambda:
|
||||
config.do_unconfigure())
|
||||
self.request.addfinalizer(config.do_unconfigure)
|
||||
return config
|
||||
|
||||
def getitem(self, source, funcname="test_func"):
|
||||
@@ -425,9 +446,8 @@ class TmpTestdir:
|
||||
env['PYTHONPATH'] = os.pathsep.join(filter(None, [
|
||||
str(os.getcwd()), env.get('PYTHONPATH', '')]))
|
||||
kw['env'] = env
|
||||
#print "env", env
|
||||
return py.std.subprocess.Popen(cmdargs,
|
||||
stdout=stdout, stderr=stderr, **kw)
|
||||
return subprocess.Popen(cmdargs,
|
||||
stdout=stdout, stderr=stderr, **kw)
|
||||
|
||||
def run(self, *cmdargs):
|
||||
return self._run(*cmdargs)
|
||||
@@ -472,12 +492,12 @@ class TmpTestdir:
|
||||
|
||||
def _getpybinargs(self, scriptname):
|
||||
if not self.request.config.getvalue("notoolsonpath"):
|
||||
# XXX we rely on script refering to the correct environment
|
||||
# we cannot use "(py.std.sys.executable,script)"
|
||||
# becaue on windows the script is e.g. a py.test.exe
|
||||
return (py.std.sys.executable, _pytest_fullpath,)
|
||||
# XXX we rely on script referring to the correct environment
|
||||
# we cannot use "(sys.executable,script)"
|
||||
# because on windows the script is e.g. a py.test.exe
|
||||
return (sys.executable, _pytest_fullpath,) # noqa
|
||||
else:
|
||||
py.test.skip("cannot run %r with --no-tools-on-path" % scriptname)
|
||||
pytest.skip("cannot run %r with --no-tools-on-path" % scriptname)
|
||||
|
||||
def runpython(self, script, prepend=True):
|
||||
if prepend:
|
||||
@@ -495,7 +515,7 @@ class TmpTestdir:
|
||||
|
||||
def runpython_c(self, command):
|
||||
command = self._getsysprepend() + command
|
||||
return self.run(py.std.sys.executable, "-c", command)
|
||||
return self.run(sys.executable, "-c", command)
|
||||
|
||||
def runpytest(self, *args):
|
||||
p = py.path.local.make_numbered_dir(prefix="runpytest-",
|
||||
@@ -514,22 +534,23 @@ class TmpTestdir:
|
||||
|
||||
def spawn_pytest(self, string, expect_timeout=10.0):
|
||||
if self.request.config.getvalue("notoolsonpath"):
|
||||
py.test.skip("--no-tools-on-path prevents running pexpect-spawn tests")
|
||||
pytest.skip("--no-tools-on-path prevents running pexpect-spawn tests")
|
||||
basetemp = self.tmpdir.mkdir("pexpect")
|
||||
invoke = " ".join(map(str, self._getpybinargs("py.test")))
|
||||
cmd = "%s --basetemp=%s %s" % (invoke, basetemp, string)
|
||||
return self.spawn(cmd, expect_timeout=expect_timeout)
|
||||
|
||||
def spawn(self, cmd, expect_timeout=10.0):
|
||||
pexpect = py.test.importorskip("pexpect", "2.4")
|
||||
if hasattr(sys, 'pypy_version_info') and '64' in py.std.platform.machine():
|
||||
pexpect = pytest.importorskip("pexpect", "3.0")
|
||||
if hasattr(sys, 'pypy_version_info') and '64' in platform.machine():
|
||||
pytest.skip("pypy-64 bit not supported")
|
||||
if sys.platform == "darwin":
|
||||
pytest.xfail("pexpect does not work reliably on darwin?!")
|
||||
if sys.platform.startswith("freebsd"):
|
||||
pytest.xfail("pexpect does not work reliably on freebsd")
|
||||
logfile = self.tmpdir.join("spawn.out")
|
||||
child = pexpect.spawn(cmd, logfile=logfile.open("w"))
|
||||
logfile = self.tmpdir.join("spawn.out").open("wb")
|
||||
child = pexpect.spawn(cmd, logfile=logfile)
|
||||
self.request.addfinalizer(logfile.close)
|
||||
child.timeout = expect_timeout
|
||||
return child
|
||||
|
||||
@@ -540,86 +561,6 @@ def getdecoded(out):
|
||||
return "INTERNAL not-utf8-decodeable, truncated string:\n%s" % (
|
||||
py.io.saferepr(out),)
|
||||
|
||||
class ReportRecorder(object):
|
||||
def __init__(self, hook):
|
||||
self.hook = hook
|
||||
self.pluginmanager = hook._pm
|
||||
self.pluginmanager.register(self)
|
||||
|
||||
def getcall(self, name):
|
||||
return self.hookrecorder.getcall(name)
|
||||
|
||||
def popcall(self, name):
|
||||
return self.hookrecorder.popcall(name)
|
||||
|
||||
def getcalls(self, names):
|
||||
""" return list of ParsedCall instances matching the given eventname. """
|
||||
return self.hookrecorder.getcalls(names)
|
||||
|
||||
# functionality for test reports
|
||||
|
||||
def getreports(self, names="pytest_runtest_logreport pytest_collectreport"):
|
||||
return [x.report for x in self.getcalls(names)]
|
||||
|
||||
def matchreport(self, inamepart="",
|
||||
names="pytest_runtest_logreport pytest_collectreport", when=None):
|
||||
""" return a testreport whose dotted import path matches """
|
||||
l = []
|
||||
for rep in self.getreports(names=names):
|
||||
try:
|
||||
if not when and rep.when != "call" and rep.passed:
|
||||
# setup/teardown passing reports - let's ignore those
|
||||
continue
|
||||
except AttributeError:
|
||||
pass
|
||||
if when and getattr(rep, 'when', None) != when:
|
||||
continue
|
||||
if not inamepart or inamepart in rep.nodeid.split("::"):
|
||||
l.append(rep)
|
||||
if not l:
|
||||
raise ValueError("could not find test report matching %r: no test reports at all!" %
|
||||
(inamepart,))
|
||||
if len(l) > 1:
|
||||
raise ValueError("found more than one testreport matching %r: %s" %(
|
||||
inamepart, l))
|
||||
return l[0]
|
||||
|
||||
def getfailures(self, names='pytest_runtest_logreport pytest_collectreport'):
|
||||
return [rep for rep in self.getreports(names) if rep.failed]
|
||||
|
||||
def getfailedcollections(self):
|
||||
return self.getfailures('pytest_collectreport')
|
||||
|
||||
def listoutcomes(self):
|
||||
passed = []
|
||||
skipped = []
|
||||
failed = []
|
||||
for rep in self.getreports(
|
||||
"pytest_collectreport pytest_runtest_logreport"):
|
||||
if rep.passed:
|
||||
if getattr(rep, "when", None) == "call":
|
||||
passed.append(rep)
|
||||
elif rep.skipped:
|
||||
skipped.append(rep)
|
||||
elif rep.failed:
|
||||
failed.append(rep)
|
||||
return passed, skipped, failed
|
||||
|
||||
def countoutcomes(self):
|
||||
return [len(x) for x in self.listoutcomes()]
|
||||
|
||||
def assertoutcome(self, passed=0, skipped=0, failed=0):
|
||||
realpassed, realskipped, realfailed = self.listoutcomes()
|
||||
assert passed == len(realpassed)
|
||||
assert skipped == len(realskipped)
|
||||
assert failed == len(realfailed)
|
||||
|
||||
def clear(self):
|
||||
self.hookrecorder.calls[:] = []
|
||||
|
||||
def unregister(self):
|
||||
self.pluginmanager.unregister(self)
|
||||
self.hookrecorder.finish_recording()
|
||||
|
||||
class LineComp:
|
||||
def __init__(self):
|
||||
@@ -668,7 +609,7 @@ class LineMatcher:
|
||||
|
||||
def fnmatch_lines(self, lines2):
|
||||
def show(arg1, arg2):
|
||||
py.builtin.print_(arg1, arg2, file=py.std.sys.stderr)
|
||||
py.builtin.print_(arg1, arg2, file=sys.stderr)
|
||||
lines2 = self._getlines(lines2)
|
||||
lines1 = self.lines[:]
|
||||
nextline = None
|
||||
@@ -692,4 +633,4 @@ class LineMatcher:
|
||||
show(" and:", repr(nextline))
|
||||
extralines.append(nextline)
|
||||
else:
|
||||
py.test.fail("remains unmatched: %r, see stderr" % (line,))
|
||||
pytest.fail("remains unmatched: %r, see stderr" % (line,))
|
||||
|
||||
1072
_pytest/python.py
1072
_pytest/python.py
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,8 @@
|
||||
""" recording warnings during test function execution. """
|
||||
|
||||
import py
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
|
||||
def pytest_funcarg__recwarn(request):
|
||||
"""Return a WarningsRecorder instance that provides these methods:
|
||||
@@ -13,7 +14,6 @@ def pytest_funcarg__recwarn(request):
|
||||
on warning categories.
|
||||
"""
|
||||
if sys.version_info >= (2,7):
|
||||
import warnings
|
||||
oldfilters = warnings.filters[:]
|
||||
warnings.simplefilter('default')
|
||||
def reset_filters():
|
||||
@@ -30,26 +30,24 @@ def deprecated_call(func, *args, **kwargs):
|
||||
""" assert that calling ``func(*args, **kwargs)``
|
||||
triggers a DeprecationWarning.
|
||||
"""
|
||||
warningmodule = py.std.warnings
|
||||
l = []
|
||||
oldwarn_explicit = getattr(warningmodule, 'warn_explicit')
|
||||
oldwarn_explicit = getattr(warnings, 'warn_explicit')
|
||||
def warn_explicit(*args, **kwargs):
|
||||
l.append(args)
|
||||
oldwarn_explicit(*args, **kwargs)
|
||||
oldwarn = getattr(warningmodule, 'warn')
|
||||
oldwarn = getattr(warnings, 'warn')
|
||||
def warn(*args, **kwargs):
|
||||
l.append(args)
|
||||
oldwarn(*args, **kwargs)
|
||||
|
||||
warningmodule.warn_explicit = warn_explicit
|
||||
warningmodule.warn = warn
|
||||
warnings.warn_explicit = warn_explicit
|
||||
warnings.warn = warn
|
||||
try:
|
||||
ret = func(*args, **kwargs)
|
||||
finally:
|
||||
warningmodule.warn_explicit = warn_explicit
|
||||
warningmodule.warn = warn
|
||||
warnings.warn_explicit = warn_explicit
|
||||
warnings.warn = warn
|
||||
if not l:
|
||||
#print warningmodule
|
||||
__tracebackhide__ = True
|
||||
raise AssertionError("%r did not produce DeprecationWarning" %(func,))
|
||||
return ret
|
||||
@@ -65,7 +63,6 @@ class RecordedWarning:
|
||||
|
||||
class WarningsRecorder:
|
||||
def __init__(self):
|
||||
warningmodule = py.std.warnings
|
||||
self.list = []
|
||||
def showwarning(message, category, filename, lineno, line=0):
|
||||
self.list.append(RecordedWarning(
|
||||
@@ -76,8 +73,8 @@ class WarningsRecorder:
|
||||
except TypeError:
|
||||
# < python2.6
|
||||
self.old_showwarning(message, category, filename, lineno)
|
||||
self.old_showwarning = warningmodule.showwarning
|
||||
warningmodule.showwarning = showwarning
|
||||
self.old_showwarning = warnings.showwarning
|
||||
warnings.showwarning = showwarning
|
||||
|
||||
def pop(self, cls=Warning):
|
||||
""" pop the first recorded warning, raise exception if not exists."""
|
||||
@@ -88,7 +85,6 @@ class WarningsRecorder:
|
||||
assert 0, "%r not found in %r" %(cls, self.list)
|
||||
|
||||
#def resetregistry(self):
|
||||
# import warnings
|
||||
# warnings.onceregistry.clear()
|
||||
# warnings.__warningregistry__.clear()
|
||||
|
||||
@@ -96,4 +92,4 @@ class WarningsRecorder:
|
||||
self.list[:] = []
|
||||
|
||||
def finalize(self):
|
||||
py.std.warnings.showwarning = self.old_showwarning
|
||||
warnings.showwarning = self.old_showwarning
|
||||
|
||||
@@ -3,10 +3,11 @@ text file.
|
||||
"""
|
||||
|
||||
import py
|
||||
import os
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting", "resultlog plugin options")
|
||||
group.addoption('--resultlog', '--result-log', action="store",
|
||||
group.addoption('--resultlog', '--result-log', action="store",
|
||||
metavar="path", default=None,
|
||||
help="path for machine-readable result log.")
|
||||
|
||||
@@ -14,6 +15,9 @@ def pytest_configure(config):
|
||||
resultlog = config.option.resultlog
|
||||
# prevent opening resultlog on slave nodes (xdist)
|
||||
if resultlog and not hasattr(config, 'slaveinput'):
|
||||
dirname = os.path.dirname(os.path.abspath(resultlog))
|
||||
if not os.path.isdir(dirname):
|
||||
os.makedirs(dirname)
|
||||
logfile = open(resultlog, 'w', 1) # line buffered
|
||||
config._resultlog = ResultLog(config, logfile)
|
||||
config.pluginmanager.register(config._resultlog)
|
||||
@@ -85,7 +89,7 @@ class ResultLog(object):
|
||||
if not report.passed:
|
||||
if report.failed:
|
||||
code = "F"
|
||||
longrepr = str(report.longrepr.reprcrash)
|
||||
longrepr = str(report.longrepr)
|
||||
else:
|
||||
assert report.skipped
|
||||
code = "S"
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
""" basic collect and runtest protocol implementations """
|
||||
|
||||
import py, sys
|
||||
import bdb
|
||||
import sys
|
||||
from time import time
|
||||
|
||||
import py
|
||||
import pytest
|
||||
from py._code.code import TerminalRepr
|
||||
|
||||
def pytest_namespace():
|
||||
@@ -83,7 +86,17 @@ def pytest_runtest_setup(item):
|
||||
item.session._setupstate.prepare(item)
|
||||
|
||||
def pytest_runtest_call(item):
|
||||
item.runtest()
|
||||
try:
|
||||
item.runtest()
|
||||
except Exception:
|
||||
# Store trace info to allow postmortem debugging
|
||||
type, value, tb = sys.exc_info()
|
||||
tb = tb.tb_next # Skip *this* frame
|
||||
sys.last_type = type
|
||||
sys.last_value = value
|
||||
sys.last_traceback = tb
|
||||
del tb # Get rid of it in this namespace
|
||||
raise
|
||||
|
||||
def pytest_runtest_teardown(item, nextitem):
|
||||
item.session._setupstate.teardown_exact(item, nextitem)
|
||||
@@ -116,7 +129,7 @@ def check_interactive_exception(call, report):
|
||||
return call.excinfo and not (
|
||||
hasattr(report, "wasxfail") or
|
||||
call.excinfo.errisinstance(skip.Exception) or
|
||||
call.excinfo.errisinstance(py.std.bdb.BdbQuit))
|
||||
call.excinfo.errisinstance(bdb.BdbQuit))
|
||||
|
||||
def call_runtest_hook(item, when, **kwds):
|
||||
hookname = "pytest_runtest_" + when
|
||||
@@ -133,14 +146,13 @@ class CallInfo:
|
||||
self.when = when
|
||||
self.start = time()
|
||||
try:
|
||||
try:
|
||||
self.result = func()
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
self.excinfo = py.code.ExceptionInfo()
|
||||
finally:
|
||||
self.result = func()
|
||||
except KeyboardInterrupt:
|
||||
self.stop = time()
|
||||
raise
|
||||
except:
|
||||
self.excinfo = py.code.ExceptionInfo()
|
||||
self.stop = time()
|
||||
|
||||
def __repr__(self):
|
||||
if self.excinfo:
|
||||
@@ -176,6 +188,11 @@ class BaseReport(object):
|
||||
except UnicodeEncodeError:
|
||||
out.line("<unprintable longrepr>")
|
||||
|
||||
def get_sections(self, prefix):
|
||||
for name, content in self.sections:
|
||||
if name.startswith(prefix):
|
||||
yield prefix, content
|
||||
|
||||
passed = property(lambda x: x.outcome == "passed")
|
||||
failed = property(lambda x: x.outcome == "failed")
|
||||
skipped = property(lambda x: x.outcome == "skipped")
|
||||
@@ -189,15 +206,15 @@ def pytest_runtest_makereport(item, call):
|
||||
duration = call.stop-call.start
|
||||
keywords = dict([(x,1) for x in item.keywords])
|
||||
excinfo = call.excinfo
|
||||
sections = []
|
||||
if not call.excinfo:
|
||||
outcome = "passed"
|
||||
longrepr = None
|
||||
else:
|
||||
excinfo = call.excinfo
|
||||
if not isinstance(excinfo, py.code.ExceptionInfo):
|
||||
outcome = "failed"
|
||||
longrepr = excinfo
|
||||
elif excinfo.errisinstance(py.test.skip.Exception):
|
||||
elif excinfo.errisinstance(pytest.skip.Exception):
|
||||
outcome = "skipped"
|
||||
r = excinfo._getreprcrash()
|
||||
longrepr = (str(r.path), r.lineno, r.message)
|
||||
@@ -208,16 +225,18 @@ def pytest_runtest_makereport(item, call):
|
||||
else: # exception in setup or teardown
|
||||
longrepr = item._repr_failure_py(excinfo,
|
||||
style=item.config.option.tbstyle)
|
||||
for rwhen, key, content in item._report_sections:
|
||||
sections.append(("Captured std%s %s" %(key, rwhen), content))
|
||||
return TestReport(item.nodeid, item.location,
|
||||
keywords, outcome, longrepr, when,
|
||||
duration=duration)
|
||||
sections, duration)
|
||||
|
||||
class TestReport(BaseReport):
|
||||
""" Basic test report object (also used for setup and teardown calls if
|
||||
they fail).
|
||||
"""
|
||||
def __init__(self, nodeid, location,
|
||||
keywords, outcome, longrepr, when, sections=(), duration=0, **extra):
|
||||
def __init__(self, nodeid, location, keywords, outcome,
|
||||
longrepr, when, sections=(), duration=0, **extra):
|
||||
#: normalized collection node id
|
||||
self.nodeid = nodeid
|
||||
|
||||
@@ -266,7 +285,9 @@ def pytest_make_collect_report(collector):
|
||||
if not call.excinfo:
|
||||
outcome = "passed"
|
||||
else:
|
||||
if call.excinfo.errisinstance(collector.skip_exceptions):
|
||||
from _pytest import nose
|
||||
skip_exceptions = (Skipped,) + nose.get_skip_exceptions()
|
||||
if call.excinfo.errisinstance(skip_exceptions):
|
||||
outcome = "skipped"
|
||||
r = collector._repr_failure_py(call.excinfo, "line").reprcrash
|
||||
longrepr = (str(r.path), r.lineno, r.message)
|
||||
@@ -283,7 +304,8 @@ def pytest_make_collect_report(collector):
|
||||
|
||||
|
||||
class CollectReport(BaseReport):
|
||||
def __init__(self, nodeid, outcome, longrepr, result, sections=(), **extra):
|
||||
def __init__(self, nodeid, outcome, longrepr, result,
|
||||
sections=(), **extra):
|
||||
self.nodeid = nodeid
|
||||
self.outcome = outcome
|
||||
self.longrepr = longrepr
|
||||
@@ -315,11 +337,10 @@ class SetupState(object):
|
||||
""" attach a finalizer to the given colitem.
|
||||
if colitem is None, this will add a finalizer that
|
||||
is called at the end of teardown_all().
|
||||
if colitem is a tuple, it will be used as a key
|
||||
and needs an explicit call to _callfinalizers(key) later on.
|
||||
"""
|
||||
assert hasattr(finalizer, '__call__')
|
||||
#assert colitem in self.stack
|
||||
assert colitem and not isinstance(colitem, tuple)
|
||||
assert py.builtin.callable(finalizer)
|
||||
#assert colitem in self.stack # some unit tests don't setup stack :/
|
||||
self._finalizers.setdefault(colitem, []).append(finalizer)
|
||||
|
||||
def _pop_and_teardown(self):
|
||||
@@ -328,9 +349,18 @@ class SetupState(object):
|
||||
|
||||
def _callfinalizers(self, colitem):
|
||||
finalizers = self._finalizers.pop(colitem, None)
|
||||
exc = None
|
||||
while finalizers:
|
||||
fin = finalizers.pop()
|
||||
fin()
|
||||
try:
|
||||
fin()
|
||||
except Exception:
|
||||
# XXX Only first exception will be seen by user,
|
||||
# ideally all should be reported.
|
||||
if exc is None:
|
||||
exc = sys.exc_info()
|
||||
if exc:
|
||||
py.builtin._reraise(*exc)
|
||||
|
||||
def _teardown_with_finalization(self, colitem):
|
||||
self._callfinalizers(colitem)
|
||||
@@ -410,7 +440,7 @@ class Skipped(OutcomeException):
|
||||
__module__ = 'builtins'
|
||||
|
||||
class Failed(OutcomeException):
|
||||
""" raised from an explicit call to py.test.fail() """
|
||||
""" raised from an explicit call to pytest.fail() """
|
||||
__module__ = 'builtins'
|
||||
|
||||
class Exit(KeyboardInterrupt):
|
||||
@@ -430,7 +460,7 @@ exit.Exception = Exit
|
||||
|
||||
def skip(msg=""):
|
||||
""" skip an executing test with the given message. Note: it's usually
|
||||
better to use the py.test.mark.skipif marker to declare a test to be
|
||||
better to use the pytest.mark.skipif marker to declare a test to be
|
||||
skipped under certain conditions like mismatching platforms or
|
||||
dependencies. See the pytest_skipping plugin for details.
|
||||
"""
|
||||
@@ -450,25 +480,25 @@ fail.Exception = Failed
|
||||
|
||||
|
||||
def importorskip(modname, minversion=None):
|
||||
""" return imported module if it has a higher __version__ than the
|
||||
optionally specified 'minversion' - otherwise call py.test.skip()
|
||||
with a message detailing the mismatch.
|
||||
""" return imported module if it has at least "minversion" as its
|
||||
__version__ attribute. If no minversion is specified the a skip
|
||||
is only triggered if the module can not be imported.
|
||||
Note that version comparison only works with simple version strings
|
||||
like "1.2.3" but not "1.2.3.dev1" or others.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
compile(modname, '', 'eval') # to catch syntaxerrors
|
||||
try:
|
||||
__import__(modname)
|
||||
except ImportError:
|
||||
py.test.skip("could not import %r" %(modname,))
|
||||
skip("could not import %r" %(modname,))
|
||||
mod = sys.modules[modname]
|
||||
if minversion is None:
|
||||
return mod
|
||||
verattr = getattr(mod, '__version__', None)
|
||||
if isinstance(minversion, str):
|
||||
minver = minversion.split(".")
|
||||
else:
|
||||
minver = list(minversion)
|
||||
if verattr is None or verattr.split(".") < minver:
|
||||
py.test.skip("module %r has __version__ %r, required is: %r" %(
|
||||
modname, verattr, minversion))
|
||||
def intver(verstring):
|
||||
return [int(x) for x in verstring.split(".")]
|
||||
if verattr is None or intver(verattr) < intver(minversion):
|
||||
skip("module %r has __version__ %r, required is: %r" %(
|
||||
modname, verattr, minversion))
|
||||
return mod
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
""" support for skip/xfail functions and markers. """
|
||||
|
||||
import py, pytest
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import py
|
||||
import pytest
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general")
|
||||
@@ -10,6 +13,14 @@ def pytest_addoption(parser):
|
||||
help="run tests even if they are marked xfail")
|
||||
|
||||
def pytest_configure(config):
|
||||
if config.option.runxfail:
|
||||
old = pytest.xfail
|
||||
config._cleanup.append(lambda: setattr(pytest, "xfail", old))
|
||||
def nop(*args, **kwargs):
|
||||
pass
|
||||
nop.Exception = XFailed
|
||||
setattr(pytest, "xfail", nop)
|
||||
|
||||
config.addinivalue_line("markers",
|
||||
"skipif(condition): skip the given test function if eval(condition) "
|
||||
"results in a True value. Evaluation happens within the "
|
||||
@@ -18,18 +29,20 @@ def pytest_configure(config):
|
||||
"http://pytest.org/latest/skipping.html"
|
||||
)
|
||||
config.addinivalue_line("markers",
|
||||
"xfail(condition, reason=None, run=True): mark the the test function "
|
||||
"xfail(condition, reason=None, run=True, raises=None): mark the the test function "
|
||||
"as an expected failure if eval(condition) has a True value. "
|
||||
"Optionally specify a reason for better reporting and run=False if "
|
||||
"you don't even want to execute the test function. See "
|
||||
"http://pytest.org/latest/skipping.html"
|
||||
"you don't even want to execute the test function. If only specific "
|
||||
"exception(s) are expected, you can list them in raises, and if the test fails "
|
||||
"in other ways, it will be reported as a true failure. "
|
||||
"See http://pytest.org/latest/skipping.html"
|
||||
)
|
||||
|
||||
def pytest_namespace():
|
||||
return dict(xfail=xfail)
|
||||
|
||||
class XFailed(pytest.fail.Exception):
|
||||
""" raised from an explicit call to py.test.xfail() """
|
||||
""" raised from an explicit call to pytest.xfail() """
|
||||
|
||||
def xfail(reason=""):
|
||||
""" xfail an executing test or setup functions with the given reason."""
|
||||
@@ -44,7 +57,8 @@ class MarkEvaluator:
|
||||
|
||||
@property
|
||||
def holder(self):
|
||||
return self.item.keywords.get(self.name, None)
|
||||
return self.item.keywords.get(self.name)
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self.holder)
|
||||
__nonzero__ = __bool__
|
||||
@@ -52,18 +66,22 @@ class MarkEvaluator:
|
||||
def wasvalid(self):
|
||||
return not hasattr(self, 'exc')
|
||||
|
||||
def invalidraise(self, exc):
|
||||
raises = self.get('raises')
|
||||
if not raises:
|
||||
return
|
||||
return not isinstance(exc, raises)
|
||||
|
||||
def istrue(self):
|
||||
try:
|
||||
return self._istrue()
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
except Exception:
|
||||
self.exc = sys.exc_info()
|
||||
if isinstance(self.exc[1], SyntaxError):
|
||||
msg = [" " * (self.exc[1].offset + 4) + "^",]
|
||||
msg.append("SyntaxError: invalid syntax")
|
||||
else:
|
||||
msg = py.std.traceback.format_exception_only(*self.exc[:2])
|
||||
msg = traceback.format_exception_only(*self.exc[:2])
|
||||
pytest.fail("Error evaluating %r expression\n"
|
||||
" %s\n"
|
||||
"%s"
|
||||
@@ -71,7 +89,7 @@ class MarkEvaluator:
|
||||
pytrace=False)
|
||||
|
||||
def _getglobals(self):
|
||||
d = {'os': py.std.os, 'sys': py.std.sys, 'config': self.item.config}
|
||||
d = {'os': os, 'sys': sys, 'config': self.item.config}
|
||||
func = self.item.obj
|
||||
try:
|
||||
d.update(func.__globals__)
|
||||
@@ -117,11 +135,10 @@ class MarkEvaluator:
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_runtest_setup(item):
|
||||
if not isinstance(item, pytest.Function):
|
||||
return
|
||||
evalskip = MarkEvaluator(item, 'skipif')
|
||||
if evalskip.istrue():
|
||||
py.test.skip(evalskip.getexplanation())
|
||||
item._evalskip = evalskip
|
||||
pytest.skip(evalskip.getexplanation())
|
||||
item._evalxfail = MarkEvaluator(item, 'xfail')
|
||||
check_xfail_no_run(item)
|
||||
|
||||
@@ -133,44 +150,42 @@ def check_xfail_no_run(item):
|
||||
evalxfail = item._evalxfail
|
||||
if evalxfail.istrue():
|
||||
if not evalxfail.get('run', True):
|
||||
py.test.xfail("[NOTRUN] " + evalxfail.getexplanation())
|
||||
pytest.xfail("[NOTRUN] " + evalxfail.getexplanation())
|
||||
|
||||
def pytest_runtest_makereport(__multicall__, item, call):
|
||||
if not isinstance(item, pytest.Function):
|
||||
return
|
||||
@pytest.mark.hookwrapper
|
||||
def pytest_runtest_makereport(item, call):
|
||||
outcome = yield
|
||||
rep = outcome.get_result()
|
||||
evalxfail = getattr(item, '_evalxfail', None)
|
||||
evalskip = getattr(item, '_evalskip', None)
|
||||
# unitttest special case, see setting of _unexpectedsuccess
|
||||
if hasattr(item, '_unexpectedsuccess'):
|
||||
rep = __multicall__.execute()
|
||||
if rep.when == "call":
|
||||
# we need to translate into how py.test encodes xpass
|
||||
rep.wasxfail = "reason: " + repr(item._unexpectedsuccess)
|
||||
rep.outcome = "failed"
|
||||
return rep
|
||||
if not (call.excinfo and
|
||||
call.excinfo.errisinstance(py.test.xfail.Exception)):
|
||||
evalxfail = getattr(item, '_evalxfail', None)
|
||||
if not evalxfail:
|
||||
return
|
||||
if call.excinfo and call.excinfo.errisinstance(py.test.xfail.Exception):
|
||||
if not item.config.getvalue("runxfail"):
|
||||
rep = __multicall__.execute()
|
||||
rep.wasxfail = "reason: " + call.excinfo.value.msg
|
||||
rep.outcome = "skipped"
|
||||
return rep
|
||||
rep = __multicall__.execute()
|
||||
evalxfail = item._evalxfail
|
||||
if not rep.skipped:
|
||||
if not item.config.option.runxfail:
|
||||
if evalxfail.wasvalid() and evalxfail.istrue():
|
||||
if call.excinfo:
|
||||
rep.outcome = "skipped"
|
||||
elif call.when == "call":
|
||||
rep.outcome = "failed"
|
||||
else:
|
||||
return rep
|
||||
if hasattr(item, '_unexpectedsuccess') and rep.when == "call":
|
||||
# we need to translate into how pytest encodes xpass
|
||||
rep.wasxfail = "reason: " + repr(item._unexpectedsuccess)
|
||||
rep.outcome = "failed"
|
||||
elif item.config.option.runxfail:
|
||||
pass # don't interefere
|
||||
elif call.excinfo and call.excinfo.errisinstance(pytest.xfail.Exception):
|
||||
rep.wasxfail = "reason: " + call.excinfo.value.msg
|
||||
rep.outcome = "skipped"
|
||||
elif evalxfail and not rep.skipped and evalxfail.wasvalid() and \
|
||||
evalxfail.istrue():
|
||||
if call.excinfo:
|
||||
if evalxfail.invalidraise(call.excinfo.value):
|
||||
rep.outcome = "failed"
|
||||
else:
|
||||
rep.outcome = "skipped"
|
||||
rep.wasxfail = evalxfail.getexplanation()
|
||||
return rep
|
||||
return rep
|
||||
elif call.when == "call":
|
||||
rep.outcome = "failed" # xpass outcome
|
||||
rep.wasxfail = evalxfail.getexplanation()
|
||||
elif evalskip is not None and rep.skipped and type(rep.longrepr) is tuple:
|
||||
# skipped by mark.skipif; change the location of the failure
|
||||
# to point to the item definition, otherwise it will display
|
||||
# the location of where the skip exception was raised within pytest
|
||||
filename, line, reason = rep.longrepr
|
||||
filename, line = item.location[:2]
|
||||
rep.longrepr = filename, line, reason
|
||||
|
||||
# called by terminalreporter progress reporting
|
||||
def pytest_report_teststatus(report):
|
||||
@@ -178,7 +193,7 @@ def pytest_report_teststatus(report):
|
||||
if report.skipped:
|
||||
return "xfailed", "x", "xfail"
|
||||
elif report.failed:
|
||||
return "xpassed", "X", "XPASS"
|
||||
return "xpassed", "X", ("XPASS", {'yellow': True})
|
||||
|
||||
# called by the terminalreporter instance/plugin
|
||||
def pytest_terminal_summary(terminalreporter):
|
||||
@@ -209,18 +224,17 @@ def pytest_terminal_summary(terminalreporter):
|
||||
tr._tw.line(line)
|
||||
|
||||
def show_simple(terminalreporter, lines, stat, format):
|
||||
tw = terminalreporter._tw
|
||||
failed = terminalreporter.stats.get(stat)
|
||||
if failed:
|
||||
for rep in failed:
|
||||
pos = rep.nodeid
|
||||
lines.append(format %(pos, ))
|
||||
pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid)
|
||||
lines.append(format %(pos,))
|
||||
|
||||
def show_xfailed(terminalreporter, lines):
|
||||
xfailed = terminalreporter.stats.get("xfailed")
|
||||
if xfailed:
|
||||
for rep in xfailed:
|
||||
pos = rep.nodeid
|
||||
pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid)
|
||||
reason = rep.wasxfail
|
||||
lines.append("XFAIL %s" % (pos,))
|
||||
if reason:
|
||||
@@ -230,7 +244,7 @@ def show_xpassed(terminalreporter, lines):
|
||||
xpassed = terminalreporter.stats.get("xpassed")
|
||||
if xpassed:
|
||||
for rep in xpassed:
|
||||
pos = rep.nodeid
|
||||
pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid)
|
||||
reason = rep.wasxfail
|
||||
lines.append("XPASS %s %s" %(pos, reason))
|
||||
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
#! /usr/bin/env python
|
||||
|
||||
# Hi There!
|
||||
# You may be wondering what this giant blob of binary data here is, you might
|
||||
# even be worried that we're up to something nefarious (good for you for being
|
||||
# paranoid!). This is a base64 encoding of a zip file, this zip file contains
|
||||
# a fully functional basic pytest script.
|
||||
#
|
||||
# Pytest is a thing that tests packages, pytest itself is a package that some-
|
||||
# one might want to install, especially if they're looking to run tests inside
|
||||
# some package they want to install. Pytest has a lot of code to collect and
|
||||
# execute tests, and other such sort of "tribal knowledge" that has been en-
|
||||
# coded in its code base. Because of this we basically include a basic copy
|
||||
# of pytest inside this blob. We do this because it let's you as a maintainer
|
||||
# or application developer who wants people who don't deal with python much to
|
||||
# easily run tests without installing the complete pytest package.
|
||||
#
|
||||
# If you're wondering how this is created: you can create it yourself if you
|
||||
# have a complete pytest installation by using this command on the command-
|
||||
# line: ``py.test --genscript=runtests.py``.
|
||||
|
||||
sources = """
|
||||
@SOURCES@"""
|
||||
|
||||
@@ -39,7 +58,7 @@ class DictImporter(object):
|
||||
if is_pkg:
|
||||
module.__path__ = [fullname]
|
||||
|
||||
do_exec(co, module.__dict__)
|
||||
do_exec(co, module.__dict__) # noqa
|
||||
return sys.modules[fullname]
|
||||
|
||||
def get_source(self, name):
|
||||
@@ -63,4 +82,4 @@ if __name__ == "__main__":
|
||||
sys.meta_path.insert(0, importer)
|
||||
|
||||
entry = "@ENTRY@"
|
||||
do_exec(entry, locals())
|
||||
do_exec(entry, locals()) # noqa
|
||||
|
||||
@@ -5,7 +5,8 @@ This is a good source for looking at the various reporting hooks.
|
||||
import pytest
|
||||
import py
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting", "reporting", after="general")
|
||||
@@ -16,7 +17,7 @@ def pytest_addoption(parser):
|
||||
group._addoption('-r',
|
||||
action="store", dest="reportchars", default=None, metavar="chars",
|
||||
help="show extra test summary info as specified by chars (f)ailed, "
|
||||
"(E)error, (s)skipped, (x)failed, (X)passed.")
|
||||
"(E)error, (s)skipped, (x)failed, (X)passed (w)warnings.")
|
||||
group._addoption('-l', '--showlocals',
|
||||
action="store_true", dest="showlocals", default=False,
|
||||
help="show locals in tracebacks (disabled by default).")
|
||||
@@ -24,12 +25,16 @@ def pytest_addoption(parser):
|
||||
action="store", dest="report", default=None, metavar="opts",
|
||||
help="(deprecated, use -r)")
|
||||
group._addoption('--tb', metavar="style",
|
||||
action="store", dest="tbstyle", default='long',
|
||||
choices=['long', 'short', 'no', 'line', 'native'],
|
||||
help="traceback print mode (long/short/line/native/no).")
|
||||
action="store", dest="tbstyle", default='auto',
|
||||
choices=['auto', 'long', 'short', 'no', 'line', 'native'],
|
||||
help="traceback print mode (auto/long/short/line/native/no).")
|
||||
group._addoption('--fulltrace', '--full-trace',
|
||||
action="store_true", default=False,
|
||||
help="don't cut any tracebacks (default is to cut).")
|
||||
group._addoption('--color', metavar="color",
|
||||
action="store", dest="color", default='auto',
|
||||
choices=['yes', 'no', 'auto'],
|
||||
help="color terminal output (yes/no/auto).")
|
||||
|
||||
def pytest_configure(config):
|
||||
config.option.verbose -= config.option.quiet
|
||||
@@ -46,7 +51,7 @@ def getreportopt(config):
|
||||
optvalue = config.option.report
|
||||
if optvalue:
|
||||
py.builtin.print_("DEPRECATED: use -r instead of --report option.",
|
||||
file=py.std.sys.stderr)
|
||||
file=sys.stderr)
|
||||
if optvalue:
|
||||
for setting in optvalue.split(","):
|
||||
setting = setting.strip()
|
||||
@@ -72,6 +77,14 @@ def pytest_report_teststatus(report):
|
||||
letter = "f"
|
||||
return report.outcome, letter, report.outcome.upper()
|
||||
|
||||
class WarningReport:
|
||||
def __init__(self, code, message, nodeid=None, fslocation=None):
|
||||
self.code = code
|
||||
self.message = message
|
||||
self.nodeid = nodeid
|
||||
self.fslocation = fslocation
|
||||
|
||||
|
||||
class TerminalReporter:
|
||||
def __init__(self, config, file=None):
|
||||
self.config = config
|
||||
@@ -82,10 +95,14 @@ class TerminalReporter:
|
||||
self._numcollected = 0
|
||||
|
||||
self.stats = {}
|
||||
self.startdir = self.curdir = py.path.local()
|
||||
self.startdir = py.path.local()
|
||||
if file is None:
|
||||
file = py.std.sys.stdout
|
||||
file = sys.stdout
|
||||
self._tw = self.writer = py.io.TerminalWriter(file)
|
||||
if self.config.option.color == 'yes':
|
||||
self._tw.hasmarkup = True
|
||||
if self.config.option.color == 'no':
|
||||
self._tw.hasmarkup = False
|
||||
self.currentfspath = None
|
||||
self.reportchars = getreportopt(config)
|
||||
self.hasmarkup = self._tw.hasmarkup
|
||||
@@ -94,12 +111,12 @@ class TerminalReporter:
|
||||
char = {'xfailed': 'x', 'skipped': 's'}.get(char, char)
|
||||
return char in self.reportchars
|
||||
|
||||
def write_fspath_result(self, fspath, res):
|
||||
def write_fspath_result(self, nodeid, res):
|
||||
fspath = self.config.rootdir.join(nodeid.split("::")[0])
|
||||
if fspath != self.currentfspath:
|
||||
self.currentfspath = fspath
|
||||
#fspath = self.startdir.bestrelpath(fspath)
|
||||
fspath = self.startdir.bestrelpath(fspath)
|
||||
self._tw.line()
|
||||
#relpath = self.startdir.bestrelpath(fspath)
|
||||
self._tw.write(fspath + " ")
|
||||
self._tw.write(res)
|
||||
|
||||
@@ -121,7 +138,8 @@ class TerminalReporter:
|
||||
self._tw.write(content, **markup)
|
||||
|
||||
def write_line(self, line, **markup):
|
||||
line = str(line)
|
||||
if not py.builtin._istext(line):
|
||||
line = py.builtin.text(line, errors="replace")
|
||||
self.ensure_newline()
|
||||
self._tw.line(line, **markup)
|
||||
|
||||
@@ -140,10 +158,16 @@ class TerminalReporter:
|
||||
self._tw.line(msg, **kw)
|
||||
|
||||
def pytest_internalerror(self, excrepr):
|
||||
for line in str(excrepr).split("\n"):
|
||||
for line in py.builtin.text(excrepr).split("\n"):
|
||||
self.write_line("INTERNALERROR> " + line)
|
||||
return 1
|
||||
|
||||
def pytest_logwarning(self, code, fslocation, message, nodeid):
|
||||
warnings = self.stats.setdefault("warnings", [])
|
||||
warning = WarningReport(code=code, fslocation=fslocation,
|
||||
message=message, nodeid=nodeid)
|
||||
warnings.append(warning)
|
||||
|
||||
def pytest_plugin_registered(self, plugin):
|
||||
if self.config.option.traceconfig:
|
||||
msg = "PLUGIN registered: %s" % (plugin,)
|
||||
@@ -158,12 +182,12 @@ class TerminalReporter:
|
||||
def pytest_runtest_logstart(self, nodeid, location):
|
||||
# ensure that the path is printed before the
|
||||
# 1st test of a module starts running
|
||||
fspath = nodeid.split("::")[0]
|
||||
if self.showlongtestinfo:
|
||||
line = self._locationline(fspath, *location)
|
||||
line = self._locationline(nodeid, *location)
|
||||
self.write_ensure_prefix(line, "")
|
||||
elif self.showfspath:
|
||||
self.write_fspath_result(fspath, "")
|
||||
fsid = nodeid.split("::")[0]
|
||||
self.write_fspath_result(fsid, "")
|
||||
|
||||
def pytest_runtest_logreport(self, report):
|
||||
rep = report
|
||||
@@ -176,7 +200,7 @@ class TerminalReporter:
|
||||
return
|
||||
if self.verbosity <= 0:
|
||||
if not hasattr(rep, 'node') and self.showfspath:
|
||||
self.write_fspath_result(rep.fspath, letter)
|
||||
self.write_fspath_result(rep.nodeid, letter)
|
||||
else:
|
||||
self._tw.write(letter)
|
||||
else:
|
||||
@@ -189,7 +213,7 @@ class TerminalReporter:
|
||||
markup = {'red':True}
|
||||
elif rep.skipped:
|
||||
markup = {'yellow':True}
|
||||
line = self._locationline(str(rep.fspath), *rep.location)
|
||||
line = self._locationline(rep.nodeid, *rep.location)
|
||||
if not hasattr(rep, 'node'):
|
||||
self.write_ensure_prefix(line, word, **markup)
|
||||
#self._tw.write(word, **markup)
|
||||
@@ -213,7 +237,7 @@ class TerminalReporter:
|
||||
items = [x for x in report.result if isinstance(x, pytest.Item)]
|
||||
self._numcollected += len(items)
|
||||
if self.hasmarkup:
|
||||
#self.write_fspath_result(report.fspath, 'E')
|
||||
#self.write_fspath_result(report.nodeid, 'E')
|
||||
self.report_collect()
|
||||
|
||||
def report_collect(self, final=False):
|
||||
@@ -243,7 +267,7 @@ class TerminalReporter:
|
||||
|
||||
@pytest.mark.trylast
|
||||
def pytest_sessionstart(self, session):
|
||||
self._sessionstarttime = py.std.time.time()
|
||||
self._sessionstarttime = time.time()
|
||||
if not self.showheader:
|
||||
return
|
||||
self.write_sep("=", "test session starts", bold=True)
|
||||
@@ -252,7 +276,7 @@ class TerminalReporter:
|
||||
if hasattr(sys, 'pypy_version_info'):
|
||||
verinfo = ".".join(map(str, sys.pypy_version_info[:3]))
|
||||
msg += "[pypy-%s-%s]" % (verinfo, sys.pypy_version_info[3])
|
||||
msg += " -- pytest-%s" % (py.test.__version__)
|
||||
msg += " -- py-%s -- pytest-%s" % (py.__version__, pytest.__version__)
|
||||
if self.verbosity > 0 or self.config.option.debug or \
|
||||
getattr(self.config.option, 'pastebin', None):
|
||||
msg += " -- " + str(sys.executable)
|
||||
@@ -264,6 +288,10 @@ class TerminalReporter:
|
||||
self.write_line(line)
|
||||
|
||||
def pytest_report_header(self, config):
|
||||
inifile = ""
|
||||
if config.inifile:
|
||||
inifile = config.rootdir.bestrelpath(config.inifile)
|
||||
lines = ["rootdir: %s, inifile: %s" %(config.rootdir, inifile)]
|
||||
plugininfo = config.pluginmanager._plugin_distinfo
|
||||
if plugininfo:
|
||||
l = []
|
||||
@@ -272,7 +300,8 @@ class TerminalReporter:
|
||||
if name.startswith("pytest-"):
|
||||
name = name[7:]
|
||||
l.append(name)
|
||||
return "plugins: %s" % ", ".join(l)
|
||||
lines.append("plugins: %s" % ", ".join(l))
|
||||
return lines
|
||||
|
||||
def pytest_collection_finish(self, session):
|
||||
if self.config.option.collectonly:
|
||||
@@ -321,13 +350,15 @@ class TerminalReporter:
|
||||
indent = (len(stack) - 1) * " "
|
||||
self._tw.line("%s%s" % (indent, col))
|
||||
|
||||
def pytest_sessionfinish(self, exitstatus, __multicall__):
|
||||
__multicall__.execute()
|
||||
@pytest.mark.hookwrapper
|
||||
def pytest_sessionfinish(self, exitstatus):
|
||||
outcome = yield
|
||||
outcome.get_result()
|
||||
self._tw.line("")
|
||||
if exitstatus in (0, 1, 2, 4):
|
||||
self.summary_errors()
|
||||
self.summary_failures()
|
||||
self.summary_hints()
|
||||
self.summary_warnings()
|
||||
self.config.hook.pytest_terminal_summary(terminalreporter=self)
|
||||
if exitstatus == 2:
|
||||
self._report_keyboardinterrupt()
|
||||
@@ -352,20 +383,24 @@ class TerminalReporter:
|
||||
else:
|
||||
excrepr.reprcrash.toterminal(self._tw)
|
||||
|
||||
def _locationline(self, collect_fspath, fspath, lineno, domain):
|
||||
def _locationline(self, nodeid, fspath, lineno, domain):
|
||||
def mkrel(nodeid):
|
||||
line = self.config.cwd_relative_nodeid(nodeid)
|
||||
if domain and line.endswith(domain):
|
||||
line = line[:-len(domain)]
|
||||
l = domain.split("[")
|
||||
l[0] = l[0].replace('.', '::') # don't replace '.' in params
|
||||
line += "[".join(l)
|
||||
return line
|
||||
# collect_fspath comes from testid which has a "/"-normalized path
|
||||
if fspath and fspath.replace("\\", "/") != collect_fspath:
|
||||
fspath = "%s <- %s" % (collect_fspath, fspath)
|
||||
|
||||
if fspath:
|
||||
line = str(fspath)
|
||||
if lineno is not None:
|
||||
lineno += 1
|
||||
line += ":" + str(lineno)
|
||||
if domain:
|
||||
line += ": " + str(domain)
|
||||
res = mkrel(nodeid).replace("::()", "") # parens-normalization
|
||||
if nodeid.split("::")[0] != fspath.replace("\\", "/"):
|
||||
res += " <- " + self.startdir.bestrelpath(fspath)
|
||||
else:
|
||||
line = "[location]"
|
||||
return line + " "
|
||||
res = "[location]"
|
||||
return res + " "
|
||||
|
||||
def _getfailureheadline(self, rep):
|
||||
if hasattr(rep, 'location'):
|
||||
@@ -393,10 +428,15 @@ class TerminalReporter:
|
||||
l.append(x)
|
||||
return l
|
||||
|
||||
def summary_hints(self):
|
||||
if self.config.option.traceconfig:
|
||||
for hint in self.config.pluginmanager._hints:
|
||||
self._tw.line("hint: %s" % hint)
|
||||
def summary_warnings(self):
|
||||
if self.hasopt("w"):
|
||||
warnings = self.stats.get("warnings")
|
||||
if not warnings:
|
||||
return
|
||||
self.write_sep("=", "warning summary")
|
||||
for w in warnings:
|
||||
self._tw.line("W%s %s %s" % (w.code,
|
||||
w.fslocation, w.message))
|
||||
|
||||
def summary_failures(self):
|
||||
if self.config.option.tbstyle != "no":
|
||||
@@ -440,9 +480,10 @@ class TerminalReporter:
|
||||
self._tw.line(content)
|
||||
|
||||
def summary_stats(self):
|
||||
session_duration = py.std.time.time() - self._sessionstarttime
|
||||
session_duration = time.time() - self._sessionstarttime
|
||||
|
||||
keys = "failed passed skipped deselected xfailed xpassed".split()
|
||||
keys = ("failed passed skipped deselected "
|
||||
"xfailed xpassed warnings").split()
|
||||
for key in self.stats.keys():
|
||||
if key not in keys:
|
||||
keys.append(key)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
""" support for providing temporary directories to test functions. """
|
||||
import pytest, py
|
||||
import re
|
||||
|
||||
import pytest
|
||||
import py
|
||||
from _pytest.monkeypatch import monkeypatch
|
||||
|
||||
|
||||
class TempdirHandler:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
@@ -63,7 +67,7 @@ def tmpdir(request):
|
||||
path object.
|
||||
"""
|
||||
name = request.node.name
|
||||
name = py.std.re.sub("[\W]", "_", name)
|
||||
name = re.sub("[\W]", "_", name)
|
||||
MAXVAL = 30
|
||||
if len(name) > MAXVAL:
|
||||
name = name[:MAXVAL]
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
""" discovery and running of std-library "unittest" style tests. """
|
||||
import pytest, py
|
||||
from __future__ import absolute_import
|
||||
import traceback
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import py
|
||||
|
||||
|
||||
# for transfering markers
|
||||
from _pytest.python import transfer_markers
|
||||
|
||||
|
||||
def is_unittest(obj):
|
||||
"""Is obj a subclass of unittest.TestCase?"""
|
||||
unittest = sys.modules.get('unittest')
|
||||
if unittest is None:
|
||||
return # nobody can have derived unittest.TestCase
|
||||
try:
|
||||
return issubclass(obj, unittest.TestCase)
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
return False
|
||||
from _pytest.skipping import MarkEvaluator
|
||||
|
||||
|
||||
def pytest_pycollect_makeitem(collector, name, obj):
|
||||
if is_unittest(obj):
|
||||
return UnitTestCase(name, parent=collector)
|
||||
# has unittest been imported and is obj a subclass of its TestCase?
|
||||
try:
|
||||
if not issubclass(obj, sys.modules["unittest"].TestCase):
|
||||
return
|
||||
except Exception:
|
||||
return
|
||||
# yes, so let's collect it
|
||||
return UnitTestCase(name, parent=collector)
|
||||
|
||||
|
||||
class UnitTestCase(pytest.Class):
|
||||
@@ -41,17 +40,18 @@ class UnitTestCase(pytest.Class):
|
||||
super(UnitTestCase, self).setup()
|
||||
|
||||
def collect(self):
|
||||
self.session._fixturemanager.parsefactories(self, unittest=True)
|
||||
loader = py.std.unittest.TestLoader()
|
||||
module = self.getparent(pytest.Module).obj
|
||||
from unittest import TestLoader
|
||||
cls = self.obj
|
||||
if not getattr(cls, "__test__", True):
|
||||
return
|
||||
self.session._fixturemanager.parsefactories(self, unittest=True)
|
||||
loader = TestLoader()
|
||||
module = self.getparent(pytest.Module).obj
|
||||
foundsomething = False
|
||||
for name in loader.getTestCaseNames(self.obj):
|
||||
x = getattr(self.obj, name)
|
||||
funcobj = getattr(x, 'im_func', x)
|
||||
transfer_markers(funcobj, cls, module)
|
||||
if hasattr(funcobj, 'todo'):
|
||||
pytest.mark.xfail(reason=str(funcobj.todo))(funcobj)
|
||||
yield TestCaseFunction(name, parent=self)
|
||||
foundsomething = True
|
||||
|
||||
@@ -70,10 +70,6 @@ class TestCaseFunction(pytest.Function):
|
||||
def setup(self):
|
||||
self._testcase = self.parent.obj(self.name)
|
||||
self._obj = getattr(self._testcase, self.name)
|
||||
if hasattr(self._testcase, 'skip'):
|
||||
pytest.skip(self._testcase.skip)
|
||||
if hasattr(self._obj, 'skip'):
|
||||
pytest.skip(self._obj.skip)
|
||||
if hasattr(self._testcase, 'setup_method'):
|
||||
self._testcase.setup_method(self._obj)
|
||||
if hasattr(self, "_request"):
|
||||
@@ -94,7 +90,7 @@ class TestCaseFunction(pytest.Function):
|
||||
except TypeError:
|
||||
try:
|
||||
try:
|
||||
l = py.std.traceback.format_exception(*rawexcinfo)
|
||||
l = traceback.format_exception(*rawexcinfo)
|
||||
l.insert(0, "NOTE: Incompatible Exception Representation, "
|
||||
"displaying natively:\n\n")
|
||||
pytest.fail("".join(l), pytrace=False)
|
||||
@@ -118,6 +114,8 @@ class TestCaseFunction(pytest.Function):
|
||||
try:
|
||||
pytest.skip(reason)
|
||||
except pytest.skip.Exception:
|
||||
self._evalskip = MarkEvaluator(self, 'SkipTest')
|
||||
self._evalskip.result = True
|
||||
self._addexcinfo(sys.exc_info())
|
||||
|
||||
def addExpectedFailure(self, testcase, rawexcinfo, reason=""):
|
||||
@@ -156,30 +154,33 @@ def pytest_runtest_makereport(item, call):
|
||||
pass
|
||||
|
||||
# twisted trial support
|
||||
def pytest_runtest_protocol(item, __multicall__):
|
||||
if isinstance(item, TestCaseFunction):
|
||||
if 'twisted.trial.unittest' in sys.modules:
|
||||
ut = sys.modules['twisted.python.failure']
|
||||
Failure__init__ = ut.Failure.__init__.im_func
|
||||
check_testcase_implements_trial_reporter()
|
||||
def excstore(self, exc_value=None, exc_type=None, exc_tb=None,
|
||||
captureVars=None):
|
||||
if exc_value is None:
|
||||
self._rawexcinfo = sys.exc_info()
|
||||
else:
|
||||
if exc_type is None:
|
||||
exc_type = type(exc_value)
|
||||
self._rawexcinfo = (exc_type, exc_value, exc_tb)
|
||||
try:
|
||||
Failure__init__(self, exc_value, exc_type, exc_tb,
|
||||
captureVars=captureVars)
|
||||
except TypeError:
|
||||
Failure__init__(self, exc_value, exc_type, exc_tb)
|
||||
ut.Failure.__init__ = excstore
|
||||
|
||||
@pytest.mark.hookwrapper
|
||||
def pytest_runtest_protocol(item):
|
||||
if isinstance(item, TestCaseFunction) and \
|
||||
'twisted.trial.unittest' in sys.modules:
|
||||
ut = sys.modules['twisted.python.failure']
|
||||
Failure__init__ = ut.Failure.__init__
|
||||
check_testcase_implements_trial_reporter()
|
||||
def excstore(self, exc_value=None, exc_type=None, exc_tb=None,
|
||||
captureVars=None):
|
||||
if exc_value is None:
|
||||
self._rawexcinfo = sys.exc_info()
|
||||
else:
|
||||
if exc_type is None:
|
||||
exc_type = type(exc_value)
|
||||
self._rawexcinfo = (exc_type, exc_value, exc_tb)
|
||||
try:
|
||||
return __multicall__.execute()
|
||||
finally:
|
||||
ut.Failure.__init__ = Failure__init__
|
||||
Failure__init__(self, exc_value, exc_type, exc_tb,
|
||||
captureVars=captureVars)
|
||||
except TypeError:
|
||||
Failure__init__(self, exc_value, exc_type, exc_tb)
|
||||
ut.Failure.__init__ = excstore
|
||||
yield
|
||||
ut.Failure.__init__ = Failure__init__
|
||||
else:
|
||||
yield
|
||||
|
||||
|
||||
def check_testcase_implements_trial_reporter(done=[]):
|
||||
if done:
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import sys
|
||||
|
||||
if __name__ == '__main__':
|
||||
import cProfile
|
||||
import py
|
||||
import pytest
|
||||
import pstats
|
||||
stats = cProfile.run('py.test.cmdline.main(["empty.py", ])', 'prof')
|
||||
script = sys.argv[1:] if len(sys.argv) > 1 else "empty.py"
|
||||
stats = cProfile.run('pytest.cmdline.main(%r)' % script, 'prof')
|
||||
p = pstats.Stats("prof")
|
||||
p.strip_dirs()
|
||||
p.sort_stats('cumulative')
|
||||
print(p.print_stats(50))
|
||||
print(p.print_stats(500))
|
||||
|
||||
12
bench/manyparam.py
Normal file
12
bench/manyparam.py
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
import pytest
|
||||
|
||||
@pytest.fixture(scope='module', params=range(966))
|
||||
def foo(request):
|
||||
return request.param
|
||||
|
||||
def test_it(foo):
|
||||
pass
|
||||
def test_it2(foo):
|
||||
pass
|
||||
|
||||
10
bench/skip.py
Normal file
10
bench/skip.py
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
SKIP = True
|
||||
|
||||
@pytest.mark.parametrize("x", xrange(5000))
|
||||
def test_foo(x):
|
||||
if SKIP:
|
||||
pytest.skip("heh")
|
||||
@@ -12,46 +12,42 @@ PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
SITETARGET=latest
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
|
||||
|
||||
regen:
|
||||
PYTHONDONTWRITEBYTECODE=1 COLUMNS=76 regendoc --update *.txt */*.txt
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " showtarget to show the pytest.org target directory"
|
||||
@echo " install to install docs to pytest.org/SITETARGET"
|
||||
@echo " install-ldf to install the doc pdf to pytest.org/SITETARGET"
|
||||
@echo " regen to regenerate pytest examples using the installed pytest"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
|
||||
clean:
|
||||
-rm -rf $(BUILDDIR)/*
|
||||
|
||||
SITETARGET=$(shell ./_getdoctarget.py)
|
||||
|
||||
showtarget:
|
||||
@echo $(SITETARGET)
|
||||
|
||||
install: html
|
||||
rsync -avz _build/html/ pytest.org:/www/pytest.org/$(SITETARGET)
|
||||
# for access talk to someone with login rights to
|
||||
# pytest-dev@pytest.org to add your ssh key
|
||||
rsync -avz _build/html/ pytest-dev@pytest.org:pytest.org/$(SITETARGET)
|
||||
|
||||
installpdf: latexpdf
|
||||
@scp $(BUILDDIR)/latex/pytest.pdf pytest.org:/www/pytest.org/$(SITETARGET)
|
||||
@scp $(BUILDDIR)/latex/pytest.pdf pytest-dev@pytest.org:pytest.org/$(SITETARGET)
|
||||
|
||||
installall: clean install installpdf
|
||||
@echo "done"
|
||||
|
||||
regen:
|
||||
PYTHONDONTWRITEBYTECODE=1 COLUMNS=76 regendoc --update *.txt */*.txt
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
|
||||
16
doc/en/_getdoctarget.py
Executable file
16
doc/en/_getdoctarget.py
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import py
|
||||
|
||||
def get_version_string():
|
||||
fn = py.path.local(__file__).join("..", "..", "..",
|
||||
"_pytest", "__init__.py")
|
||||
for line in fn.readlines():
|
||||
if "version" in line:
|
||||
return eval(line.split("=")[-1])
|
||||
|
||||
def get_minor_version_string():
|
||||
return ".".join(get_version_string().split(".")[:2])
|
||||
|
||||
if __name__ == "__main__":
|
||||
print (get_minor_version_string())
|
||||
@@ -1,339 +0,0 @@
|
||||
/*
|
||||
* sphinxdoc.css_t
|
||||
* ~~~~~~~~~~~~~~~
|
||||
*
|
||||
* Sphinx stylesheet -- sphinxdoc theme. Originally created by
|
||||
* Armin Ronacher for Werkzeug.
|
||||
*
|
||||
* :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
|
||||
* :license: BSD, see LICENSE for details.
|
||||
*
|
||||
*/
|
||||
|
||||
@import url("basic.css");
|
||||
|
||||
/* -- page layout ----------------------------------------------------------- */
|
||||
|
||||
body {
|
||||
font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva',
|
||||
'Verdana', sans-serif;
|
||||
font-size: 1.1em;
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 150%;
|
||||
text-align: center;
|
||||
background-color: #BFD1D4;
|
||||
color: black;
|
||||
padding: 0;
|
||||
border: 1px solid #aaa;
|
||||
|
||||
margin: 0px 80px 0px 80px;
|
||||
min-width: 740px;
|
||||
}
|
||||
|
||||
div.document {
|
||||
background-color: white;
|
||||
text-align: left;
|
||||
background-image: url(contents.png);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
|
||||
div.bodywrapper {
|
||||
margin: 0 240px 0 0;
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
|
||||
div.body {
|
||||
margin: 0;
|
||||
padding: 0.5em 20px 20px 20px;
|
||||
}
|
||||
|
||||
div.related {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
div.related ul {
|
||||
background-image: url(navigation.png);
|
||||
height: 2em;
|
||||
border-top: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
div.related ul li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 2em;
|
||||
float: left;
|
||||
}
|
||||
|
||||
div.related ul li.right {
|
||||
float: right;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
div.related ul li a {
|
||||
margin: 0;
|
||||
padding: 0 5px 0 5px;
|
||||
line-height: 1.75em;
|
||||
color: #EE9816;
|
||||
}
|
||||
|
||||
div.related ul li a:hover {
|
||||
color: #3CA8E7;
|
||||
}
|
||||
|
||||
div.sphinxsidebarwrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.sphinxsidebar {
|
||||
margin: 0;
|
||||
padding: 0.5em 15px 15px 0;
|
||||
width: 210px;
|
||||
float: right;
|
||||
font-size: 1em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3, div.sphinxsidebar h4 {
|
||||
margin: 1em 0 0.5em 0;
|
||||
font-size: 1em;
|
||||
padding: 0.1em 0 0.1em 0.5em;
|
||||
color: white;
|
||||
border: 1px solid #86989B;
|
||||
background-color: #AFC1C4;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3 a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul {
|
||||
padding-left: 1.5em;
|
||||
margin-top: 7px;
|
||||
padding: 0;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul ul {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
div.footer {
|
||||
background-color: #E3EFF1;
|
||||
color: #86989B;
|
||||
padding: 3px 8px 3px 0;
|
||||
clear: both;
|
||||
font-size: 0.8em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div.footer a {
|
||||
color: #86989B;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* -- body styles ----------------------------------------------------------- */
|
||||
|
||||
p {
|
||||
margin: 0.8em 0 0.5em 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #CA7900;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #2491CF;
|
||||
}
|
||||
|
||||
div.body a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
padding: 0.7em 0 0.3em 0;
|
||||
font-size: 1.5em;
|
||||
color: #11557C;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 1.3em 0 0.2em 0;
|
||||
font-size: 1.35em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 1em 0 -0.3em 0;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
div.body h1 a, div.body h2 a, div.body h3 a, div.body h4 a, div.body h5 a, div.body h6 a {
|
||||
color: black!important;
|
||||
}
|
||||
|
||||
h1 a.anchor, h2 a.anchor, h3 a.anchor, h4 a.anchor, h5 a.anchor, h6 a.anchor {
|
||||
display: none;
|
||||
margin: 0 0 0 0.3em;
|
||||
padding: 0 0.2em 0 0.2em;
|
||||
color: #aaa!important;
|
||||
}
|
||||
|
||||
h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor,
|
||||
h5:hover a.anchor, h6:hover a.anchor {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
h1 a.anchor:hover, h2 a.anchor:hover, h3 a.anchor:hover, h4 a.anchor:hover,
|
||||
h5 a.anchor:hover, h6 a.anchor:hover {
|
||||
color: #777;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
a.headerlink {
|
||||
color: #c60f0f!important;
|
||||
font-size: 1em;
|
||||
margin-left: 6px;
|
||||
padding: 0 4px 0 4px;
|
||||
text-decoration: none!important;
|
||||
}
|
||||
|
||||
a.headerlink:hover {
|
||||
background-color: #ccc;
|
||||
color: white!important;
|
||||
}
|
||||
|
||||
cite, code, tt {
|
||||
font-family: 'Consolas', 'Deja Vu Sans Mono',
|
||||
'Bitstream Vera Sans Mono', monospace;
|
||||
font-size: 0.95em;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
tt {
|
||||
background-color: #f2f2f2;
|
||||
border-bottom: 1px solid #ddd;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
tt.descname, tt.descclassname, tt.xref {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1px solid #abc;
|
||||
margin: 2em;
|
||||
}
|
||||
|
||||
a tt {
|
||||
border: 0;
|
||||
color: #CA7900;
|
||||
}
|
||||
|
||||
a tt:hover {
|
||||
color: #2491CF;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: 'Consolas', 'Deja Vu Sans Mono',
|
||||
'Bitstream Vera Sans Mono', monospace;
|
||||
font-size: 0.95em;
|
||||
letter-spacing: 0.015em;
|
||||
line-height: 120%;
|
||||
padding: 0.5em;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
pre a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
td.linenos pre {
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
|
||||
div.quotebar {
|
||||
background-color: #f8f8f8;
|
||||
max-width: 250px;
|
||||
float: right;
|
||||
padding: 2px 7px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
div.topic {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
margin: 0 -0.5em 0 -0.5em;
|
||||
}
|
||||
|
||||
table td, table th {
|
||||
padding: 0.2em 0.5em 0.2em 0.5em;
|
||||
}
|
||||
|
||||
div.admonition, div.warning {
|
||||
font-size: 0.9em;
|
||||
margin: 1em 0 1em 0;
|
||||
border: 1px solid #86989B;
|
||||
background-color: #f7f7f7;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.admonition p, div.warning p {
|
||||
margin: 0.5em 1em 0.5em 1em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.admonition pre, div.warning pre {
|
||||
margin: 0.4em 1em 0.4em 1em;
|
||||
}
|
||||
|
||||
div.admonition p.admonition-title,
|
||||
div.warning p.admonition-title {
|
||||
margin: 0;
|
||||
padding: 0.1em 0 0.1em 0.5em;
|
||||
color: white;
|
||||
border-bottom: 1px solid #86989B;
|
||||
font-weight: bold;
|
||||
background-color: #AFC1C4;
|
||||
}
|
||||
|
||||
div.warning {
|
||||
border: 1px solid #940000;
|
||||
}
|
||||
|
||||
div.warning p.admonition-title {
|
||||
background-color: #CF0000;
|
||||
border-bottom-color: #940000;
|
||||
}
|
||||
|
||||
div.admonition ul, div.admonition ol,
|
||||
div.warning ul, div.warning ol {
|
||||
margin: 0.1em 0.5em 0.5em 3em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.versioninfo {
|
||||
margin: 1em 0 0 0;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #DDEAF0;
|
||||
padding: 8px;
|
||||
line-height: 1.3em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.viewcode-back {
|
||||
font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva',
|
||||
'Verdana', sans-serif;
|
||||
}
|
||||
|
||||
div.viewcode-block:target {
|
||||
background-color: #f4debf;
|
||||
border-top: 1px solid #ac9;
|
||||
border-bottom: 1px solid #ac9;
|
||||
}
|
||||
17
doc/en/_templates/globaltoc.html
Normal file
17
doc/en/_templates/globaltoc.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<h3><a href="{{ pathto(master_doc) }}">{{ _('Table Of Contents') }}</a></h3>
|
||||
|
||||
<ul>
|
||||
<li><a href="{{ pathto('index') }}">Home</a></li>
|
||||
<li><a href="{{ pathto('contents') }}">Contents</a></li>
|
||||
<li><a href="{{ pathto('getting-started') }}">Install</a></li>
|
||||
<li><a href="{{ pathto('example/index') }}">Examples</a></li>
|
||||
<li><a href="{{ pathto('customize') }}">Customize</a></li>
|
||||
<li><a href="{{ pathto('contact') }}">Contact</a></li>
|
||||
<li><a href="{{ pathto('talks') }}">Talks/Posts</a></li>
|
||||
<li><a href="{{ pathto('changelog') }}">Changelog</a></li>
|
||||
</ul>
|
||||
|
||||
{%- if display_toc %}
|
||||
<hr>
|
||||
{{ toc }}
|
||||
{%- endif %}
|
||||
@@ -1,22 +0,0 @@
|
||||
<h3>Download</h3>
|
||||
{% if version.endswith('(hg)') %}
|
||||
<p>This documentation is for version <b>{{ version }}</b>, which is
|
||||
not released yet.</p>
|
||||
<p>You can use it from the
|
||||
<a href="http://bitbucket.org/hpk42/pytest">Bitbucket Repo</a> or look for
|
||||
released versions in the <a href="http://pypi.python.org/pypi/pytest">Python
|
||||
Package Index</a>.</p>
|
||||
{% else %}
|
||||
<p><b><a href="{{ pathto('announce/index')}}">{{ release }} release</a></b>
|
||||
[<a href="{{ pathto('changelog') }}">Changelog</a>]</p>
|
||||
<p>
|
||||
<a href="http://pypi.python.org/pypi/pytest">pytest/PyPI</a>
|
||||
</p>
|
||||
<pre>easy_install pytest</pre>
|
||||
<pre>pip install pytest</pre>
|
||||
{% endif %}
|
||||
|
||||
<h3>Questions? Suggestions?</h3>
|
||||
|
||||
<p><a href="{{ pathto('contact') }}">contact channels</a>
|
||||
</p>
|
||||
@@ -1,13 +1,5 @@
|
||||
{% extends "!layout.html" %}
|
||||
|
||||
{% block relbaritems %}
|
||||
{{ super() }}
|
||||
<g:plusone></g:plusone>
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
{{ super() }}
|
||||
<script type="text/javascript">
|
||||
@@ -23,5 +15,4 @@
|
||||
})();
|
||||
|
||||
</script>
|
||||
<script type="text/javascript" src="https://apis.google.com/js/plusone.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
11
doc/en/_templates/links.html
Normal file
11
doc/en/_templates/links.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<h3>Useful Links</h3>
|
||||
<ul>
|
||||
<li><a href="{{ pathto('index') }}">The pytest Website</a></li>
|
||||
<li><a href="{{ pathto('contributing') }}">Contribution Guide</a></li>
|
||||
<li><a href="https://pypi.python.org/pypi/pytest">pytest @ PyPI</a></li>
|
||||
<li><a href="https://bitbucket.org/pytest-dev/pytest/">pytest @ Bitbucket</a></li>
|
||||
<li><a href="http://pytest.org/latest/plugins_index/index.html">3rd party plugins</a></li>
|
||||
<li><a href="https://bitbucket.org/pytest-dev/pytest/issues?status=new&status=open">Issue Tracker</a></li>
|
||||
<li><a href="http://pytest.org/latest/pytest.pdf">PDF Documentation</a>
|
||||
</ul>
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
|
||||
{%- if pagename != "search" %}
|
||||
<div id="searchbox" style="display: none">
|
||||
<form class="search" action="{{ pathto('search') }}" method="get">
|
||||
<input type="text" name="q" size="18" />
|
||||
<input type="submit" value="{{ _('Search') }}" />
|
||||
<input type="hidden" name="check_keywords" value="yes" />
|
||||
<input type="hidden" name="area" value="default" />
|
||||
</form>
|
||||
</div>
|
||||
<script type="text/javascript">$('#searchbox').show(0);</script>
|
||||
{%- endif %}
|
||||
|
||||
<h3>quicklinks</h3>
|
||||
<div style="text-align: left; font-size: 100%; vertical-align: middle;">
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ pathto('index') }}">home</a>
|
||||
</td><td>
|
||||
<a href="{{ pathto('contents') }}">TOC/contents</a>
|
||||
</td></tr><tr><td>
|
||||
<a href="{{ pathto('getting-started') }}">install</a>
|
||||
</td><td>
|
||||
<a href="{{ pathto('changelog') }}">changelog</a>
|
||||
</td></tr><tr><td>
|
||||
<a href="{{ pathto('example/index') }}">examples</a>
|
||||
</td><td>
|
||||
<a href="{{ pathto('customize') }}">customize</a>
|
||||
</td></tr><tr><td>
|
||||
<a href="https://bitbucket.org/hpk42/pytest/issues?status=new&status=open">issues[bb]</a>
|
||||
</td><td>
|
||||
<a href="{{ pathto('contact') }}">contact</a>
|
||||
</td></tr><tr><td>
|
||||
<a href="{{ pathto('talks') }}">Talks/Posts</a>
|
||||
</td></tr></table>
|
||||
</div>
|
||||
{% extends "basic/localtoc.html" %}
|
||||
|
||||
5
doc/en/_templates/sidebarintro.html
Normal file
5
doc/en/_templates/sidebarintro.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<h3>About pytest</h3>
|
||||
<p>
|
||||
pytest is a mature full-featured Python testing tool that helps
|
||||
you write better programs.
|
||||
</p>
|
||||
3
doc/en/_themes/.gitignore
vendored
Normal file
3
doc/en/_themes/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*.pyc
|
||||
*.pyo
|
||||
.DS_Store
|
||||
37
doc/en/_themes/LICENSE
Normal file
37
doc/en/_themes/LICENSE
Normal file
@@ -0,0 +1,37 @@
|
||||
Copyright (c) 2010 by Armin Ronacher.
|
||||
|
||||
Some rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms of the theme, with or
|
||||
without modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
* The names of the contributors may not be used to endorse or
|
||||
promote products derived from this software without specific
|
||||
prior written permission.
|
||||
|
||||
We kindly ask you to only use these themes in an unmodified manner just
|
||||
for Flask and Flask-related products, not for unrelated projects. If you
|
||||
like the visual style and want to use it for your own projects, please
|
||||
consider making some larger changes to the themes (such as changing
|
||||
font faces, sizes, colors or margins).
|
||||
|
||||
THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
31
doc/en/_themes/README
Normal file
31
doc/en/_themes/README
Normal file
@@ -0,0 +1,31 @@
|
||||
Flask Sphinx Styles
|
||||
===================
|
||||
|
||||
This repository contains sphinx styles for Flask and Flask related
|
||||
projects. To use this style in your Sphinx documentation, follow
|
||||
this guide:
|
||||
|
||||
1. put this folder as _themes into your docs folder. Alternatively
|
||||
you can also use git submodules to check out the contents there.
|
||||
2. add this to your conf.py:
|
||||
|
||||
sys.path.append(os.path.abspath('_themes'))
|
||||
html_theme_path = ['_themes']
|
||||
html_theme = 'flask'
|
||||
|
||||
The following themes exist:
|
||||
|
||||
- 'flask' - the standard flask documentation theme for large
|
||||
projects
|
||||
- 'flask_small' - small one-page theme. Intended to be used by
|
||||
very small addon libraries for flask.
|
||||
|
||||
The following options exist for the flask_small theme:
|
||||
|
||||
[options]
|
||||
index_logo = '' filename of a picture in _static
|
||||
to be used as replacement for the
|
||||
h1 in the index.rst file.
|
||||
index_logo_height = 120px height of the index logo
|
||||
github_fork = '' repository name on github for the
|
||||
"fork me" badge
|
||||
24
doc/en/_themes/flask/layout.html
Normal file
24
doc/en/_themes/flask/layout.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{%- extends "basic/layout.html" %}
|
||||
{%- block extrahead %}
|
||||
{{ super() }}
|
||||
{% if theme_touch_icon %}
|
||||
<link rel="apple-touch-icon" href="{{ pathto('_static/' ~ theme_touch_icon, 1) }}" />
|
||||
{% endif %}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9">
|
||||
{% endblock %}
|
||||
{%- block relbar2 %}{% endblock %}
|
||||
{% block header %}
|
||||
{{ super() }}
|
||||
{% if pagename == 'index' %}
|
||||
<div class=indexwrapper>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{%- block footer %}
|
||||
<div class="footer">
|
||||
© Copyright {{ copyright }}.
|
||||
Created using <a href="http://sphinx.pocoo.org/">Sphinx</a>.
|
||||
</div>
|
||||
{% if pagename == 'index' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{%- endblock %}
|
||||
19
doc/en/_themes/flask/relations.html
Normal file
19
doc/en/_themes/flask/relations.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<h3>Related Topics</h3>
|
||||
<ul>
|
||||
<li><a href="{{ pathto(master_doc) }}">Documentation overview</a><ul>
|
||||
{%- for parent in parents %}
|
||||
<li><a href="{{ parent.link|e }}">{{ parent.title }}</a><ul>
|
||||
{%- endfor %}
|
||||
{%- if prev %}
|
||||
<li>Previous: <a href="{{ prev.link|e }}" title="{{ _('previous chapter')
|
||||
}}">{{ prev.title }}</a></li>
|
||||
{%- endif %}
|
||||
{%- if next %}
|
||||
<li>Next: <a href="{{ next.link|e }}" title="{{ _('next chapter')
|
||||
}}">{{ next.title }}</a></li>
|
||||
{%- endif %}
|
||||
{%- for parent in parents %}
|
||||
</ul></li>
|
||||
{%- endfor %}
|
||||
</ul></li>
|
||||
</ul>
|
||||
557
doc/en/_themes/flask/static/flasky.css_t
Normal file
557
doc/en/_themes/flask/static/flasky.css_t
Normal file
@@ -0,0 +1,557 @@
|
||||
/*
|
||||
* flasky.css_t
|
||||
* ~~~~~~~~~~~~
|
||||
*
|
||||
* :copyright: Copyright 2010 by Armin Ronacher.
|
||||
* :license: Flask Design License, see LICENSE for details.
|
||||
*/
|
||||
|
||||
{% set page_width = '1020px' %}
|
||||
{% set sidebar_width = '220px' %}
|
||||
/* orange of logo is #d67c29 but we use black for links for now */
|
||||
{% set link_color = '#000' %}
|
||||
{% set link_hover_color = '#000' %}
|
||||
{% set base_font = 'sans-serif' %}
|
||||
{% set header_font = 'serif' %}
|
||||
|
||||
@import url("basic.css");
|
||||
|
||||
/* -- page layout ----------------------------------------------------------- */
|
||||
|
||||
body {
|
||||
font-family: {{ base_font }};
|
||||
font-size: 17px;
|
||||
background-color: white;
|
||||
color: #000;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.document {
|
||||
width: {{ page_width }};
|
||||
margin: 30px auto 0 auto;
|
||||
}
|
||||
|
||||
div.documentwrapper {
|
||||
float: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.bodywrapper {
|
||||
margin: 0 0 0 {{ sidebar_width }};
|
||||
}
|
||||
|
||||
div.sphinxsidebar {
|
||||
width: {{ sidebar_width }};
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-top: 1px solid #B1B4B6;
|
||||
}
|
||||
|
||||
div.body {
|
||||
background-color: #ffffff;
|
||||
color: #3E4349;
|
||||
padding: 0 30px 0 30px;
|
||||
}
|
||||
|
||||
img.floatingflask {
|
||||
padding: 0 0 10px 10px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
div.footer {
|
||||
width: {{ page_width }};
|
||||
margin: 20px auto 30px auto;
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div.footer a {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
div.related {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.sphinxsidebar a {
|
||||
color: #444;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted #999;
|
||||
}
|
||||
|
||||
div.sphinxsidebar a:hover {
|
||||
border-bottom: 1px solid #999;
|
||||
}
|
||||
|
||||
div.sphinxsidebar {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
div.sphinxsidebarwrapper {
|
||||
padding: 18px 10px;
|
||||
}
|
||||
|
||||
div.sphinxsidebarwrapper p.logo {
|
||||
padding: 0 0 20px 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3,
|
||||
div.sphinxsidebar h4 {
|
||||
font-family: {{ header_font }};
|
||||
color: #444;
|
||||
font-size: 24px;
|
||||
font-weight: normal;
|
||||
margin: 0 0 5px 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h4 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3 a {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
div.sphinxsidebar p.logo a,
|
||||
div.sphinxsidebar h3 a,
|
||||
div.sphinxsidebar p.logo a:hover,
|
||||
div.sphinxsidebar h3 a:hover {
|
||||
border: none;
|
||||
}
|
||||
|
||||
div.sphinxsidebar p {
|
||||
color: #555;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul {
|
||||
margin: 10px 0;
|
||||
padding: 0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
div.sphinxsidebar input {
|
||||
border: 1px solid #ccc;
|
||||
font-family: {{ base_font }};
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* -- body styles ----------------------------------------------------------- */
|
||||
|
||||
a {
|
||||
color: {{ link_color }};
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: {{ link_hover_color }};
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a.reference.internal em {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
div.body h1,
|
||||
div.body h2,
|
||||
div.body h3,
|
||||
div.body h4,
|
||||
div.body h5,
|
||||
div.body h6 {
|
||||
font-family: {{ header_font }};
|
||||
font-weight: normal;
|
||||
margin: 30px 0px 10px 0px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
{% if theme_index_logo %}
|
||||
div.indexwrapper h1 {
|
||||
text-indent: -999999px;
|
||||
background: url({{ theme_index_logo }}) no-repeat center center;
|
||||
height: {{ theme_index_logo_height }};
|
||||
}
|
||||
{% else %}
|
||||
div.indexwrapper div.body h1 {
|
||||
font-size: 200%;
|
||||
}
|
||||
{% endif %}
|
||||
div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; }
|
||||
div.body h2 { font-size: 180%; }
|
||||
div.body h3 { font-size: 150%; }
|
||||
div.body h4 { font-size: 130%; }
|
||||
div.body h5 { font-size: 100%; }
|
||||
div.body h6 { font-size: 100%; }
|
||||
|
||||
a.headerlink {
|
||||
color: #ddd;
|
||||
padding: 0 4px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.headerlink:hover {
|
||||
color: #444;
|
||||
background: #eaeaea;
|
||||
}
|
||||
|
||||
div.body p, div.body dd, div.body li {
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
div.admonition {
|
||||
background: #fafafa;
|
||||
margin: 20px -30px;
|
||||
padding: 10px 30px;
|
||||
border-top: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
div.admonition tt.xref, div.admonition a tt {
|
||||
border-bottom: 1px solid #fafafa;
|
||||
}
|
||||
|
||||
dd div.admonition {
|
||||
margin-left: -60px;
|
||||
padding-left: 60px;
|
||||
}
|
||||
|
||||
div.admonition p.admonition-title {
|
||||
font-family: {{ header_font }};
|
||||
font-weight: normal;
|
||||
font-size: 24px;
|
||||
margin: 0 0 10px 0;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
div.admonition p.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
div.highlight {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
dt:target, .highlight {
|
||||
background: #FAF3E8;
|
||||
}
|
||||
|
||||
div.note {
|
||||
background-color: #eee;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
div.seealso {
|
||||
background-color: #ffc;
|
||||
border: 1px solid #ff6;
|
||||
}
|
||||
|
||||
div.topic {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
p.admonition-title {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
p.admonition-title:after {
|
||||
content: ":";
|
||||
}
|
||||
|
||||
pre, tt, code {
|
||||
font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
|
||||
font-size: 0.9em;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
img.screenshot {
|
||||
}
|
||||
|
||||
tt.descname, tt.descclassname {
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
tt.descname {
|
||||
padding-right: 0.08em;
|
||||
}
|
||||
|
||||
img.screenshot {
|
||||
-moz-box-shadow: 2px 2px 4px #eee;
|
||||
-webkit-box-shadow: 2px 2px 4px #eee;
|
||||
box-shadow: 2px 2px 4px #eee;
|
||||
}
|
||||
|
||||
table.docutils {
|
||||
border: 1px solid #888;
|
||||
-moz-box-shadow: 2px 2px 4px #eee;
|
||||
-webkit-box-shadow: 2px 2px 4px #eee;
|
||||
box-shadow: 2px 2px 4px #eee;
|
||||
}
|
||||
|
||||
table.docutils td, table.docutils th {
|
||||
border: 1px solid #888;
|
||||
padding: 0.25em 0.7em;
|
||||
}
|
||||
|
||||
table.field-list, table.footnote {
|
||||
border: none;
|
||||
-moz-box-shadow: none;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
table.footnote {
|
||||
margin: 15px 0;
|
||||
width: 100%;
|
||||
border: 1px solid #eee;
|
||||
background: #fdfdfd;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
table.footnote + table.footnote {
|
||||
margin-top: -15px;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
table.field-list th {
|
||||
padding: 0 0.8em 0 0;
|
||||
}
|
||||
|
||||
table.field-list td {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table.footnote td.label {
|
||||
width: 0px;
|
||||
padding: 0.3em 0 0.3em 0.5em;
|
||||
}
|
||||
|
||||
table.footnote td {
|
||||
padding: 0.3em 0.5em;
|
||||
}
|
||||
|
||||
dl {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
dl dd {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 0 30px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin: 10px 0 10px 30px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #eee;
|
||||
padding: 7px 30px;
|
||||
margin: 15px -30px;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
|
||||
dl pre, blockquote pre, li pre {
|
||||
margin-left: -60px;
|
||||
padding-left: 60px;
|
||||
}
|
||||
|
||||
dl dl pre {
|
||||
margin-left: -90px;
|
||||
padding-left: 90px;
|
||||
}
|
||||
|
||||
tt {
|
||||
background-color: #ecf0f3;
|
||||
color: #222;
|
||||
/* padding: 1px 2px; */
|
||||
}
|
||||
|
||||
tt.xref, a tt {
|
||||
background-color: #FBFBFB;
|
||||
border-bottom: 1px solid white;
|
||||
}
|
||||
|
||||
a.reference {
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted {{ link_color }};
|
||||
}
|
||||
|
||||
a.reference:hover {
|
||||
border-bottom: 1px solid {{ link_hover_color }};
|
||||
}
|
||||
|
||||
a.footnote-reference {
|
||||
text-decoration: none;
|
||||
font-size: 0.7em;
|
||||
vertical-align: top;
|
||||
border-bottom: 1px dotted {{ link_color }};
|
||||
}
|
||||
|
||||
a.footnote-reference:hover {
|
||||
border-bottom: 1px solid {{ link_hover_color }};
|
||||
}
|
||||
|
||||
a:hover tt {
|
||||
background: #EEE;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 870px) {
|
||||
|
||||
div.sphinxsidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.document {
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
div.documentwrapper {
|
||||
margin-left: 0;
|
||||
margin-top: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
div.bodywrapper {
|
||||
margin-top: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.document {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.bodywrapper {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.github {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media screen and (max-width: 875px) {
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px 30px;
|
||||
}
|
||||
|
||||
div.documentwrapper {
|
||||
float: none;
|
||||
background: white;
|
||||
}
|
||||
|
||||
div.sphinxsidebar {
|
||||
display: block;
|
||||
float: none;
|
||||
width: 102.5%;
|
||||
margin: 50px -30px -20px -30px;
|
||||
padding: 10px 20px;
|
||||
background: #333;
|
||||
color: white;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p,
|
||||
div.sphinxsidebar h3 a, div.sphinxsidebar ul {
|
||||
color: white;
|
||||
}
|
||||
|
||||
div.sphinxsidebar a {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
div.sphinxsidebar p.logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.document {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
div.related {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 10px 0 20px 0;
|
||||
}
|
||||
|
||||
div.related ul,
|
||||
div.related ul li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.bodywrapper {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
div.body {
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.rtd_doc_footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.document {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.github {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* misc. */
|
||||
|
||||
.revsys-inline {
|
||||
display: none!important;
|
||||
}
|
||||
9
doc/en/_themes/flask/theme.conf
Normal file
9
doc/en/_themes/flask/theme.conf
Normal file
@@ -0,0 +1,9 @@
|
||||
[theme]
|
||||
inherit = basic
|
||||
stylesheet = flasky.css
|
||||
pygments_style = flask_theme_support.FlaskyStyle
|
||||
|
||||
[options]
|
||||
index_logo = ''
|
||||
index_logo_height = 120px
|
||||
touch_icon =
|
||||
86
doc/en/_themes/flask_theme_support.py
Normal file
86
doc/en/_themes/flask_theme_support.py
Normal file
@@ -0,0 +1,86 @@
|
||||
# flasky extensions. flasky pygments style based on tango style
|
||||
from pygments.style import Style
|
||||
from pygments.token import Keyword, Name, Comment, String, Error, \
|
||||
Number, Operator, Generic, Whitespace, Punctuation, Other, Literal
|
||||
|
||||
|
||||
class FlaskyStyle(Style):
|
||||
background_color = "#f8f8f8"
|
||||
default_style = ""
|
||||
|
||||
styles = {
|
||||
# No corresponding class for the following:
|
||||
#Text: "", # class: ''
|
||||
Whitespace: "underline #f8f8f8", # class: 'w'
|
||||
Error: "#a40000 border:#ef2929", # class: 'err'
|
||||
Other: "#000000", # class 'x'
|
||||
|
||||
Comment: "italic #8f5902", # class: 'c'
|
||||
Comment.Preproc: "noitalic", # class: 'cp'
|
||||
|
||||
Keyword: "bold #004461", # class: 'k'
|
||||
Keyword.Constant: "bold #004461", # class: 'kc'
|
||||
Keyword.Declaration: "bold #004461", # class: 'kd'
|
||||
Keyword.Namespace: "bold #004461", # class: 'kn'
|
||||
Keyword.Pseudo: "bold #004461", # class: 'kp'
|
||||
Keyword.Reserved: "bold #004461", # class: 'kr'
|
||||
Keyword.Type: "bold #004461", # class: 'kt'
|
||||
|
||||
Operator: "#582800", # class: 'o'
|
||||
Operator.Word: "bold #004461", # class: 'ow' - like keywords
|
||||
|
||||
Punctuation: "bold #000000", # class: 'p'
|
||||
|
||||
# because special names such as Name.Class, Name.Function, etc.
|
||||
# are not recognized as such later in the parsing, we choose them
|
||||
# to look the same as ordinary variables.
|
||||
Name: "#000000", # class: 'n'
|
||||
Name.Attribute: "#c4a000", # class: 'na' - to be revised
|
||||
Name.Builtin: "#004461", # class: 'nb'
|
||||
Name.Builtin.Pseudo: "#3465a4", # class: 'bp'
|
||||
Name.Class: "#000000", # class: 'nc' - to be revised
|
||||
Name.Constant: "#000000", # class: 'no' - to be revised
|
||||
Name.Decorator: "#888", # class: 'nd' - to be revised
|
||||
Name.Entity: "#ce5c00", # class: 'ni'
|
||||
Name.Exception: "bold #cc0000", # class: 'ne'
|
||||
Name.Function: "#000000", # class: 'nf'
|
||||
Name.Property: "#000000", # class: 'py'
|
||||
Name.Label: "#f57900", # class: 'nl'
|
||||
Name.Namespace: "#000000", # class: 'nn' - to be revised
|
||||
Name.Other: "#000000", # class: 'nx'
|
||||
Name.Tag: "bold #004461", # class: 'nt' - like a keyword
|
||||
Name.Variable: "#000000", # class: 'nv' - to be revised
|
||||
Name.Variable.Class: "#000000", # class: 'vc' - to be revised
|
||||
Name.Variable.Global: "#000000", # class: 'vg' - to be revised
|
||||
Name.Variable.Instance: "#000000", # class: 'vi' - to be revised
|
||||
|
||||
Number: "#990000", # class: 'm'
|
||||
|
||||
Literal: "#000000", # class: 'l'
|
||||
Literal.Date: "#000000", # class: 'ld'
|
||||
|
||||
String: "#4e9a06", # class: 's'
|
||||
String.Backtick: "#4e9a06", # class: 'sb'
|
||||
String.Char: "#4e9a06", # class: 'sc'
|
||||
String.Doc: "italic #8f5902", # class: 'sd' - like a comment
|
||||
String.Double: "#4e9a06", # class: 's2'
|
||||
String.Escape: "#4e9a06", # class: 'se'
|
||||
String.Heredoc: "#4e9a06", # class: 'sh'
|
||||
String.Interpol: "#4e9a06", # class: 'si'
|
||||
String.Other: "#4e9a06", # class: 'sx'
|
||||
String.Regex: "#4e9a06", # class: 'sr'
|
||||
String.Single: "#4e9a06", # class: 's1'
|
||||
String.Symbol: "#4e9a06", # class: 'ss'
|
||||
|
||||
Generic: "#000000", # class: 'g'
|
||||
Generic.Deleted: "#a40000", # class: 'gd'
|
||||
Generic.Emph: "italic #000000", # class: 'ge'
|
||||
Generic.Error: "#ef2929", # class: 'gr'
|
||||
Generic.Heading: "bold #000080", # class: 'gh'
|
||||
Generic.Inserted: "#00A000", # class: 'gi'
|
||||
Generic.Output: "#888", # class: 'go'
|
||||
Generic.Prompt: "#745334", # class: 'gp'
|
||||
Generic.Strong: "bold #000000", # class: 'gs'
|
||||
Generic.Subheading: "bold #800080", # class: 'gu'
|
||||
Generic.Traceback: "bold #a40000", # class: 'gt'
|
||||
}
|
||||
78
doc/en/adopt.txt
Normal file
78
doc/en/adopt.txt
Normal file
@@ -0,0 +1,78 @@
|
||||
|
||||
April 2015 is "adopt pytest month"
|
||||
=============================================
|
||||
|
||||
Are you an enthusiastic pytest user, the local testing guru in your workplace? Or are you considering using pytest for your open source project, but not sure how to get started? Then you may be interested in "adopt pytest month"!
|
||||
|
||||
We will pair experienced pytest users with open source projects, for a month's effort of getting new development teams started with pytest.
|
||||
|
||||
In 2015 we are trying this for the first time. In February and March 2015 we will gather volunteers on both sides, in April we will do the work, and in May we will evaluate how it went. This effort is being coordinated by Brianna Laugher. If you have any questions or comments, you can raise them on the `@pytestdotorg twitter account <https://twitter.com/pytestdotorg>`_ the `issue tracker`_ or the `pytest-dev mailing list`_.
|
||||
|
||||
|
||||
.. _`issue tracker`: https://bitbucket.org/pytest-dev/pytest/issue/676/adopt-pytest-month-2015
|
||||
.. _`pytest-dev mailing list`: https://mail.python.org/mailman/listinfo/pytest-dev
|
||||
|
||||
|
||||
The ideal pytest helper
|
||||
-----------------------------------------
|
||||
|
||||
- will be able to commit 2-4 hours a week to working with their particular project (this might involve joining their mailing list, installing the software and exploring any existing tests, offering advice, writing some example tests)
|
||||
- feels confident in using pytest (e.g. has explored command line options, knows how to write parametrized tests, has an idea about conftest contents)
|
||||
- does not need to be an expert in every aspect!
|
||||
|
||||
`Pytest helpers, sign up here`_! (preferably in February, hard deadline 22 March)
|
||||
|
||||
|
||||
.. _`Pytest helpers, sign up here`: http://goo.gl/forms/nxqAhqWt1P
|
||||
|
||||
|
||||
The ideal partner project
|
||||
-----------------------------------------
|
||||
|
||||
- is open source, and predominantly written in Python
|
||||
- has an automated/documented install process for developers
|
||||
- has more than one core developer
|
||||
- has at least one official release (e.g. is available on pypi)
|
||||
- has the support of the core development team, in trying out pytest adoption
|
||||
- has no tests... or 100% test coverage... or somewhere in between!
|
||||
|
||||
`Partner projects, sign up here`_! (by 22 March)
|
||||
|
||||
|
||||
.. _`Partner projects, sign up here`: http://goo.gl/forms/ZGyqlHiwk3
|
||||
|
||||
|
||||
What does it mean to "adopt pytest"?
|
||||
-----------------------------------------
|
||||
|
||||
There can be many different definitions of "success". Pytest can run many `nose and unittest`_ tests by default, so using pytest as your testrunner may be possible from day 1. Job done, right?
|
||||
|
||||
Progressive success might look like:
|
||||
|
||||
- tests can be run (by pytest) without errors (there may be failures)
|
||||
- tests can be run (by pytest) without failures
|
||||
- test runner is integrated into CI server
|
||||
- existing tests are rewritten to take advantage of pytest features - this can happen in several iterations, for example:
|
||||
- changing to native assert_ statements (pycmd_ has a script to help with that, ``pyconvert_unittest.py``)
|
||||
- changing `setUp/tearDown methods`_ to fixtures_
|
||||
- adding markers_
|
||||
- other changes to reduce boilerplate
|
||||
- assess needs for future tests to be written, e.g. new fixtures, distributed_ testing tweaks
|
||||
|
||||
"Success" should also include that the development team feels comfortable with their knowledge of how to use pytest. In fact this is probably more important than anything else. So spending a lot of time on communication, giving examples, etc will probably be important - both in running the tests, and in writing them.
|
||||
|
||||
It may be after the month is up, the partner project decides that pytest is not right for it. That's okay - hopefully the pytest team will also learn something about its weaknesses or deficiencies.
|
||||
|
||||
.. _`nose and unittest`: faq.html#how-does-pytest-relate-to-nose-and-unittest
|
||||
.. _assert: asserts.html
|
||||
.. _pycmd: https://bitbucket.org/hpk42/pycmd/overview
|
||||
.. _`setUp/tearDown methods`: xunit_setup.html
|
||||
.. _fixtures: fixture.html
|
||||
.. _markers: markers.html
|
||||
.. _distributed: xdist.html
|
||||
|
||||
|
||||
Other ways to help
|
||||
-----------------------------------------
|
||||
|
||||
Promote! Do your favourite open source Python projects use pytest? If not, why not tell them about this page?
|
||||
@@ -5,6 +5,15 @@ Release announcements
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
release-2.7.1
|
||||
release-2.7.0
|
||||
release-2.6.3
|
||||
release-2.6.2
|
||||
release-2.6.1
|
||||
release-2.6.0
|
||||
release-2.5.2
|
||||
release-2.5.1
|
||||
release-2.5.0
|
||||
release-2.4.2
|
||||
release-2.4.1
|
||||
release-2.4.0
|
||||
|
||||
@@ -34,7 +34,7 @@ a full list of details. A few feature highlights:
|
||||
influence the environment before conftest files import ``django``.
|
||||
|
||||
- reporting: color the last line red or green depending if
|
||||
failures/errors occured or everything passed.
|
||||
failures/errors occurred or everything passed.
|
||||
|
||||
The documentation has been updated to accomodate the changes,
|
||||
see `http://pytest.org <http://pytest.org>`_
|
||||
@@ -95,7 +95,7 @@ new features:
|
||||
as strings will remain fully supported.
|
||||
|
||||
- reporting: color the last line red or green depending if
|
||||
failures/errors occured or everything passed. thanks Christian
|
||||
failures/errors occurred or everything passed. thanks Christian
|
||||
Theunert.
|
||||
|
||||
- make "import pdb ; pdb.set_trace()" work natively wrt capturing (no
|
||||
|
||||
@@ -21,7 +21,7 @@ pytest-2.4.2 is another bug-fixing release:
|
||||
|
||||
- introduce node.get_marker/node.add_marker API for plugins
|
||||
like pytest-pep8 and pytest-flakes to avoid the messy
|
||||
details of the node.keywords pseudo-dicts. Adapated
|
||||
details of the node.keywords pseudo-dicts. Adapted
|
||||
docs.
|
||||
|
||||
- remove attempt to "dup" stdout at startup as it's icky.
|
||||
|
||||
175
doc/en/announce/release-2.5.0.txt
Normal file
175
doc/en/announce/release-2.5.0.txt
Normal file
@@ -0,0 +1,175 @@
|
||||
pytest-2.5.0: now down to ZERO reported bugs!
|
||||
===========================================================================
|
||||
|
||||
pytest-2.5.0 is a big fixing release, the result of two community bug
|
||||
fixing days plus numerous additional works from many people and
|
||||
reporters. The release should be fully compatible to 2.4.2, existing
|
||||
plugins and test suites. We aim at maintaining this level of ZERO reported
|
||||
bugs because it's no fun if your testing tool has bugs, is it? Under a
|
||||
condition, though: when submitting a bug report please provide
|
||||
clear information about the circumstances and a simple example which
|
||||
reproduces the problem.
|
||||
|
||||
The issue tracker is of course not empty now. We have many remaining
|
||||
"enhacement" issues which we'll hopefully can tackle in 2014 with your
|
||||
help.
|
||||
|
||||
For those who use older Python versions, please note that pytest is not
|
||||
automatically tested on python2.5 due to virtualenv, setuptools and tox
|
||||
not supporting it anymore. Manual verification shows that it mostly
|
||||
works fine but it's not going to be part of the automated release
|
||||
process and thus likely to break in the future.
|
||||
|
||||
As usual, current docs are at
|
||||
|
||||
http://pytest.org
|
||||
|
||||
and you can upgrade from pypi via::
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Particular thanks for helping with this release go to Anatoly Bubenkoff,
|
||||
Floris Bruynooghe, Marc Abramowitz, Ralph Schmitt, Ronny Pfannschmidt,
|
||||
Donald Stufft, James Lan, Rob Dennis, Jason R. Coombs, Mathieu Agopian,
|
||||
Virgil Dupras, Bruno Oliveira, Alex Gaynor and others.
|
||||
|
||||
have fun,
|
||||
holger krekel
|
||||
|
||||
|
||||
2.5.0
|
||||
-----------------------------------
|
||||
|
||||
- dropped python2.5 from automated release testing of pytest itself
|
||||
which means it's probably going to break soon (but still works
|
||||
with this release we believe).
|
||||
|
||||
- simplified and fixed implementation for calling finalizers when
|
||||
parametrized fixtures or function arguments are involved. finalization
|
||||
is now performed lazily at setup time instead of in the "teardown phase".
|
||||
While this might sound odd at first, it helps to ensure that we are
|
||||
correctly handling setup/teardown even in complex code. User-level code
|
||||
should not be affected unless it's implementing the pytest_runtest_teardown
|
||||
hook and expecting certain fixture instances are torn down within (very
|
||||
unlikely and would have been unreliable anyway).
|
||||
|
||||
- PR90: add --color=yes|no|auto option to force terminal coloring
|
||||
mode ("auto" is default). Thanks Marc Abramowitz.
|
||||
|
||||
- fix issue319 - correctly show unicode in assertion errors. Many
|
||||
thanks to Floris Bruynooghe for the complete PR. Also means
|
||||
we depend on py>=1.4.19 now.
|
||||
|
||||
- fix issue396 - correctly sort and finalize class-scoped parametrized
|
||||
tests independently from number of methods on the class.
|
||||
|
||||
- refix issue323 in a better way -- parametrization should now never
|
||||
cause Runtime Recursion errors because the underlying algorithm
|
||||
for re-ordering tests per-scope/per-fixture is not recursive
|
||||
anymore (it was tail-call recursive before which could lead
|
||||
to problems for more than >966 non-function scoped parameters).
|
||||
|
||||
- fix issue290 - there is preliminary support now for parametrizing
|
||||
with repeated same values (sometimes useful to to test if calling
|
||||
a second time works as with the first time).
|
||||
|
||||
- close issue240 - document precisely how pytest module importing
|
||||
works, discuss the two common test directory layouts, and how it
|
||||
interacts with PEP420-namespace packages.
|
||||
|
||||
- fix issue246 fix finalizer order to be LIFO on independent fixtures
|
||||
depending on a parametrized higher-than-function scoped fixture.
|
||||
(was quite some effort so please bear with the complexity of this sentence :)
|
||||
Thanks Ralph Schmitt for the precise failure example.
|
||||
|
||||
- fix issue244 by implementing special index for parameters to only use
|
||||
indices for paramentrized test ids
|
||||
|
||||
- fix issue287 by running all finalizers but saving the exception
|
||||
from the first failing finalizer and re-raising it so teardown will
|
||||
still have failed. We reraise the first failing exception because
|
||||
it might be the cause for other finalizers to fail.
|
||||
|
||||
- fix ordering when mock.patch or other standard decorator-wrappings
|
||||
are used with test methods. This fixues issue346 and should
|
||||
help with random "xdist" collection failures. Thanks to
|
||||
Ronny Pfannschmidt and Donald Stufft for helping to isolate it.
|
||||
|
||||
- fix issue357 - special case "-k" expressions to allow for
|
||||
filtering with simple strings that are not valid python expressions.
|
||||
Examples: "-k 1.3" matches all tests parametrized with 1.3.
|
||||
"-k None" filters all tests that have "None" in their name
|
||||
and conversely "-k 'not None'".
|
||||
Previously these examples would raise syntax errors.
|
||||
|
||||
- fix issue384 by removing the trial support code
|
||||
since the unittest compat enhancements allow
|
||||
trial to handle it on its own
|
||||
|
||||
- don't hide an ImportError when importing a plugin produces one.
|
||||
fixes issue375.
|
||||
|
||||
- fix issue275 - allow usefixtures and autouse fixtures
|
||||
for running doctest text files.
|
||||
|
||||
- fix issue380 by making --resultlog only rely on longrepr instead
|
||||
of the "reprcrash" attribute which only exists sometimes.
|
||||
|
||||
- address issue122: allow @pytest.fixture(params=iterator) by exploding
|
||||
into a list early on.
|
||||
|
||||
- fix pexpect-3.0 compatibility for pytest's own tests.
|
||||
(fixes issue386)
|
||||
|
||||
- allow nested parametrize-value markers, thanks James Lan for the PR.
|
||||
|
||||
- fix unicode handling with new monkeypatch.setattr(import_path, value)
|
||||
API. Thanks Rob Dennis. Fixes issue371.
|
||||
|
||||
- fix unicode handling with junitxml, fixes issue368.
|
||||
|
||||
- In assertion rewriting mode on Python 2, fix the detection of coding
|
||||
cookies. See issue #330.
|
||||
|
||||
- make "--runxfail" turn imperative pytest.xfail calls into no ops
|
||||
(it already did neutralize pytest.mark.xfail markers)
|
||||
|
||||
- refine pytest / pkg_resources interactions: The AssertionRewritingHook
|
||||
PEP302 compliant loader now registers itself with setuptools/pkg_resources
|
||||
properly so that the pkg_resources.resource_stream method works properly.
|
||||
Fixes issue366. Thanks for the investigations and full PR to Jason R. Coombs.
|
||||
|
||||
- pytestconfig fixture is now session-scoped as it is the same object during the
|
||||
whole test run. Fixes issue370.
|
||||
|
||||
- avoid one surprising case of marker malfunction/confusion::
|
||||
|
||||
@pytest.mark.some(lambda arg: ...)
|
||||
def test_function():
|
||||
|
||||
would not work correctly because pytest assumes @pytest.mark.some
|
||||
gets a function to be decorated already. We now at least detect if this
|
||||
arg is an lambda and thus the example will work. Thanks Alex Gaynor
|
||||
for bringing it up.
|
||||
|
||||
- xfail a test on pypy that checks wrong encoding/ascii (pypy does
|
||||
not error out). fixes issue385.
|
||||
|
||||
- internally make varnames() deal with classes's __init__,
|
||||
although it's not needed by pytest itself atm. Also
|
||||
fix caching. Fixes issue376.
|
||||
|
||||
- fix issue221 - handle importing of namespace-package with no
|
||||
__init__.py properly.
|
||||
|
||||
- refactor internal FixtureRequest handling to avoid monkeypatching.
|
||||
One of the positive user-facing effects is that the "request" object
|
||||
can now be used in closures.
|
||||
|
||||
- fixed version comparison in pytest.importskip(modname, minverstring)
|
||||
|
||||
- fix issue377 by clarifying in the nose-compat docs that pytest
|
||||
does not duplicate the unittest-API into the "plain" namespace.
|
||||
|
||||
- fix verbose reporting for @mock'd test functions
|
||||
|
||||
47
doc/en/announce/release-2.5.1.txt
Normal file
47
doc/en/announce/release-2.5.1.txt
Normal file
@@ -0,0 +1,47 @@
|
||||
pytest-2.5.1: fixes and new home page styling
|
||||
===========================================================================
|
||||
|
||||
pytest is a mature Python testing tool with more than a 1000 tests
|
||||
against itself, passing on many different interpreters and platforms.
|
||||
|
||||
The 2.5.1 release maintains the "zero-reported-bugs" promise by fixing
|
||||
the three bugs reported since the last release a few days ago. It also
|
||||
features a new home page styling implemented by Tobias Bieniek, based on
|
||||
the flask theme from Armin Ronacher:
|
||||
|
||||
http://pytest.org
|
||||
|
||||
If you have anything more to improve styling and docs,
|
||||
we'd be very happy to merge further pull requests.
|
||||
|
||||
On the coding side, the release also contains a little enhancement to
|
||||
fixture decorators allowing to directly influence generation of test
|
||||
ids, thanks to Floris Bruynooghe. Other thanks for helping with
|
||||
this release go to Anatoly Bubenkoff and Ronny Pfannschmidt.
|
||||
|
||||
As usual, you can upgrade from pypi via::
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
have fun and a nice remaining "bug-free" time of the year :)
|
||||
holger krekel
|
||||
|
||||
2.5.1
|
||||
-----------------------------------
|
||||
|
||||
- merge new documentation styling PR from Tobias Bieniek.
|
||||
|
||||
- fix issue403: allow parametrize of multiple same-name functions within
|
||||
a collection node. Thanks Andreas Kloeckner and Alex Gaynor for reporting
|
||||
and analysis.
|
||||
|
||||
- Allow parameterized fixtures to specify the ID of the parameters by
|
||||
adding an ids argument to pytest.fixture() and pytest.yield_fixture().
|
||||
Thanks Floris Bruynooghe.
|
||||
|
||||
- fix issue404 by always using the binary xml escape in the junitxml
|
||||
plugin. Thanks Ronny Pfannschmidt.
|
||||
|
||||
- fix issue407: fix addoption docstring to point to argparse instead of
|
||||
optparse. Thanks Daniel D. Wright.
|
||||
|
||||
64
doc/en/announce/release-2.5.2.txt
Normal file
64
doc/en/announce/release-2.5.2.txt
Normal file
@@ -0,0 +1,64 @@
|
||||
pytest-2.5.2: fixes
|
||||
===========================================================================
|
||||
|
||||
pytest is a mature Python testing tool with more than a 1000 tests
|
||||
against itself, passing on many different interpreters and platforms.
|
||||
|
||||
The 2.5.2 release fixes a few bugs with two maybe-bugs remaining and
|
||||
actively being worked on (and waiting for the bug reporter's input).
|
||||
We also have a new contribution guide thanks to Piotr Banaszkiewicz
|
||||
and others.
|
||||
|
||||
See docs at:
|
||||
|
||||
http://pytest.org
|
||||
|
||||
As usual, you can upgrade from pypi via::
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to the following people who contributed to this release:
|
||||
|
||||
Anatoly Bubenkov
|
||||
Ronny Pfannschmidt
|
||||
Floris Bruynooghe
|
||||
Bruno Oliveira
|
||||
Andreas Pelme
|
||||
Jurko Gospodnetić
|
||||
Piotr Banaszkiewicz
|
||||
Simon Liedtke
|
||||
lakka
|
||||
Lukasz Balcerzak
|
||||
Philippe Muller
|
||||
Daniel Hahler
|
||||
|
||||
have fun,
|
||||
holger krekel
|
||||
|
||||
2.5.2
|
||||
-----------------------------------
|
||||
|
||||
- fix issue409 -- better interoperate with cx_freeze by not
|
||||
trying to import from collections.abc which causes problems
|
||||
for py27/cx_freeze. Thanks Wolfgang L. for reporting and tracking it down.
|
||||
|
||||
- fixed docs and code to use "pytest" instead of "py.test" almost everywhere.
|
||||
Thanks Jurko Gospodnetic for the complete PR.
|
||||
|
||||
- fix issue425: mention at end of "py.test -h" that --markers
|
||||
and --fixtures work according to specified test path (or current dir)
|
||||
|
||||
- fix issue413: exceptions with unicode attributes are now printed
|
||||
correctly also on python2 and with pytest-xdist runs. (the fix
|
||||
requires py-1.4.20)
|
||||
|
||||
- copy, cleanup and integrate py.io capture
|
||||
from pylib 1.4.20.dev2 (rev 13d9af95547e)
|
||||
|
||||
- address issue416: clarify docs as to conftest.py loading semantics
|
||||
|
||||
- fix issue429: comparing byte strings with non-ascii chars in assert
|
||||
expressions now work better. Thanks Floris Bruynooghe.
|
||||
|
||||
- make capfd/capsys.capture private, its unused and shouldnt be exposed
|
||||
|
||||
153
doc/en/announce/release-2.6.0.txt
Normal file
153
doc/en/announce/release-2.6.0.txt
Normal file
@@ -0,0 +1,153 @@
|
||||
pytest-2.6.0: shorter tracebacks, new warning system, test runner compat
|
||||
===========================================================================
|
||||
|
||||
pytest is a mature Python testing tool with more than a 1000 tests
|
||||
against itself, passing on many different interpreters and platforms.
|
||||
|
||||
The 2.6.0 release should be drop-in backward compatible to 2.5.2 and
|
||||
fixes a number of bugs and brings some new features, mainly:
|
||||
|
||||
- shorter tracebacks by default: only the first (test function) entry
|
||||
and the last (failure location) entry are shown, the ones between
|
||||
only in "short" format. Use ``--tb=long`` to get back the old
|
||||
behaviour of showing "long" entries everywhere.
|
||||
|
||||
- a new warning system which reports oddities during collection
|
||||
and execution. For example, ignoring collecting Test* classes with an
|
||||
``__init__`` now produces a warning.
|
||||
|
||||
- various improvements to nose/mock/unittest integration
|
||||
|
||||
Note also that 2.6.0 departs with the "zero reported bugs" policy
|
||||
because it has been too hard to keep up with it, unfortunately.
|
||||
Instead we are for now rather bound to work on "upvoted" issues in
|
||||
the https://bitbucket.org/pytest-dev/pytest/issues?status=new&status=open&sort=-votes
|
||||
issue tracker.
|
||||
|
||||
See docs at:
|
||||
|
||||
http://pytest.org
|
||||
|
||||
As usual, you can upgrade from pypi via::
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all who contributed, among them:
|
||||
|
||||
Benjamin Peterson
|
||||
Jurko Gospodnetić
|
||||
Floris Bruynooghe
|
||||
Marc Abramowitz
|
||||
Marc Schlaich
|
||||
Trevor Bekolay
|
||||
Bruno Oliveira
|
||||
Alex Groenholm
|
||||
|
||||
have fun,
|
||||
holger krekel
|
||||
|
||||
2.6.0
|
||||
-----------------------------------
|
||||
|
||||
- fix issue537: Avoid importing old assertion reinterpretation code by default.
|
||||
Thanks Benjamin Peterson.
|
||||
|
||||
- fix issue364: shorten and enhance tracebacks representation by default.
|
||||
The new "--tb=auto" option (default) will only display long tracebacks
|
||||
for the first and last entry. You can get the old behaviour of printing
|
||||
all entries as long entries with "--tb=long". Also short entries by
|
||||
default are now printed very similarly to "--tb=native" ones.
|
||||
|
||||
- fix issue514: teach assertion reinterpretation about private class attributes
|
||||
Thanks Benjamin Peterson.
|
||||
|
||||
- change -v output to include full node IDs of tests. Users can copy
|
||||
a node ID from a test run, including line number, and use it as a
|
||||
positional argument in order to run only a single test.
|
||||
|
||||
- fix issue 475: fail early and comprehensible if calling
|
||||
pytest.raises with wrong exception type.
|
||||
|
||||
- fix issue516: tell in getting-started about current dependencies.
|
||||
|
||||
- cleanup setup.py a bit and specify supported versions. Thanks Jurko
|
||||
Gospodnetic for the PR.
|
||||
|
||||
- change XPASS colour to yellow rather then red when tests are run
|
||||
with -v.
|
||||
|
||||
- fix issue473: work around mock putting an unbound method into a class
|
||||
dict when double-patching.
|
||||
|
||||
- fix issue498: if a fixture finalizer fails, make sure that
|
||||
the fixture is still invalidated.
|
||||
|
||||
- fix issue453: the result of the pytest_assertrepr_compare hook now gets
|
||||
it's newlines escaped so that format_exception does not blow up.
|
||||
|
||||
- internal new warning system: pytest will now produce warnings when
|
||||
it detects oddities in your test collection or execution.
|
||||
Warnings are ultimately sent to a new pytest_logwarning hook which is
|
||||
currently only implemented by the terminal plugin which displays
|
||||
warnings in the summary line and shows more details when -rw (report on
|
||||
warnings) is specified.
|
||||
|
||||
- change skips into warnings for test classes with an __init__ and
|
||||
callables in test modules which look like a test but are not functions.
|
||||
|
||||
- fix issue436: improved finding of initial conftest files from command
|
||||
line arguments by using the result of parse_known_args rather than
|
||||
the previous flaky heuristics. Thanks Marc Abramowitz for tests
|
||||
and initial fixing approaches in this area.
|
||||
|
||||
- fix issue #479: properly handle nose/unittest(2) SkipTest exceptions
|
||||
during collection/loading of test modules. Thanks to Marc Schlaich
|
||||
for the complete PR.
|
||||
|
||||
- fix issue490: include pytest_load_initial_conftests in documentation
|
||||
and improve docstring.
|
||||
|
||||
- fix issue472: clarify that ``pytest.config.getvalue()`` cannot work
|
||||
if it's triggered ahead of command line parsing.
|
||||
|
||||
- merge PR123: improved integration with mock.patch decorator on tests.
|
||||
|
||||
- fix issue412: messing with stdout/stderr FD-level streams is now
|
||||
captured without crashes.
|
||||
|
||||
- fix issue483: trial/py33 works now properly. Thanks Daniel Grana for PR.
|
||||
|
||||
- improve example for pytest integration with "python setup.py test"
|
||||
which now has a generic "-a" or "--pytest-args" option where you
|
||||
can pass additional options as a quoted string. Thanks Trevor Bekolay.
|
||||
|
||||
- simplified internal capturing mechanism and made it more robust
|
||||
against tests or setups changing FD1/FD2, also better integrated
|
||||
now with pytest.pdb() in single tests.
|
||||
|
||||
- improvements to pytest's own test-suite leakage detection, courtesy of PRs
|
||||
from Marc Abramowitz
|
||||
|
||||
- fix issue492: avoid leak in test_writeorg. Thanks Marc Abramowitz.
|
||||
|
||||
- fix issue493: don't run tests in doc directory with ``python setup.py test``
|
||||
(use tox -e doctesting for that)
|
||||
|
||||
- fix issue486: better reporting and handling of early conftest loading failures
|
||||
|
||||
- some cleanup and simplification of internal conftest handling.
|
||||
|
||||
- work a bit harder to break reference cycles when catching exceptions.
|
||||
Thanks Jurko Gospodnetic.
|
||||
|
||||
- fix issue443: fix skip examples to use proper comparison. Thanks Alex
|
||||
Groenholm.
|
||||
|
||||
- support nose-style ``__test__`` attribute on modules, classes and
|
||||
functions, including unittest-style Classes. If set to False, the
|
||||
test will not be collected.
|
||||
|
||||
- fix issue512: show "<notset>" for arguments which might not be set
|
||||
in monkeypatch plugin. Improves output in documentation.
|
||||
|
||||
- avoid importing "py.test" (an old alias module for "pytest")
|
||||
59
doc/en/announce/release-2.6.1.txt
Normal file
59
doc/en/announce/release-2.6.1.txt
Normal file
@@ -0,0 +1,59 @@
|
||||
pytest-2.6.1: fixes and new xfail feature
|
||||
===========================================================================
|
||||
|
||||
pytest is a mature Python testing tool with more than a 1100 tests
|
||||
against itself, passing on many different interpreters and platforms.
|
||||
The 2.6.1 release is drop-in compatible to 2.5.2 and actually fixes some
|
||||
regressions introduced with 2.6.0. It also brings a little feature
|
||||
to the xfail marker which now recognizes expected exceptions,
|
||||
see the CHANGELOG below.
|
||||
|
||||
See docs at:
|
||||
|
||||
http://pytest.org
|
||||
|
||||
As usual, you can upgrade from pypi via::
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all who contributed, among them:
|
||||
|
||||
Floris Bruynooghe
|
||||
Bruno Oliveira
|
||||
Nicolas Delaby
|
||||
|
||||
have fun,
|
||||
holger krekel
|
||||
|
||||
Changes 2.6.1
|
||||
=================
|
||||
|
||||
- No longer show line numbers in the --verbose output, the output is now
|
||||
purely the nodeid. The line number is still shown in failure reports.
|
||||
Thanks Floris Bruynooghe.
|
||||
|
||||
- fix issue437 where assertion rewriting could cause pytest-xdist slaves
|
||||
to collect different tests. Thanks Bruno Oliveira.
|
||||
|
||||
- fix issue555: add "errors" attribute to capture-streams to satisfy
|
||||
some distutils and possibly other code accessing sys.stdout.errors.
|
||||
|
||||
- fix issue547 capsys/capfd also work when output capturing ("-s") is disabled.
|
||||
|
||||
- address issue170: allow pytest.mark.xfail(...) to specify expected exceptions via
|
||||
an optional "raises=EXC" argument where EXC can be a single exception
|
||||
or a tuple of exception classes. Thanks David Mohr for the complete
|
||||
PR.
|
||||
|
||||
- fix integration of pytest with unittest.mock.patch decorator when
|
||||
it uses the "new" argument. Thanks Nicolas Delaby for test and PR.
|
||||
|
||||
- fix issue with detecting conftest files if the arguments contain
|
||||
"::" node id specifications (copy pasted from "-v" output)
|
||||
|
||||
- fix issue544 by only removing "@NUM" at the end of "::" separated parts
|
||||
and if the part has an ".py" extension
|
||||
|
||||
- don't use py.std import helper, rather import things directly.
|
||||
Thanks Bruno Oliveira.
|
||||
|
||||
52
doc/en/announce/release-2.6.2.txt
Normal file
52
doc/en/announce/release-2.6.2.txt
Normal file
@@ -0,0 +1,52 @@
|
||||
pytest-2.6.2: few fixes and cx_freeze support
|
||||
===========================================================================
|
||||
|
||||
pytest is a mature Python testing tool with more than a 1100 tests
|
||||
against itself, passing on many different interpreters and platforms.
|
||||
This release is drop-in compatible to 2.5.2 and 2.6.X. It also
|
||||
brings support for including pytest with cx_freeze or similar
|
||||
freezing tools into your single-file app distribution. For details
|
||||
see the CHANGELOG below.
|
||||
|
||||
See docs at:
|
||||
|
||||
http://pytest.org
|
||||
|
||||
As usual, you can upgrade from pypi via::
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all who contributed, among them:
|
||||
|
||||
Floris Bruynooghe
|
||||
Benjamin Peterson
|
||||
Bruno Oliveira
|
||||
|
||||
have fun,
|
||||
holger krekel
|
||||
|
||||
2.6.2
|
||||
-----------
|
||||
|
||||
- Added function pytest.freeze_includes(), which makes it easy to embed
|
||||
pytest into executables using tools like cx_freeze.
|
||||
See docs for examples and rationale. Thanks Bruno Oliveira.
|
||||
|
||||
- Improve assertion rewriting cache invalidation precision.
|
||||
|
||||
- fixed issue561: adapt autouse fixture example for python3.
|
||||
|
||||
- fixed issue453: assertion rewriting issue with __repr__ containing
|
||||
"\n{", "\n}" and "\n~".
|
||||
|
||||
- fix issue560: correctly display code if an "else:" or "finally:" is
|
||||
followed by statements on the same line.
|
||||
|
||||
- Fix example in monkeypatch documentation, thanks t-8ch.
|
||||
|
||||
- fix issue572: correct tmpdir doc example for python3.
|
||||
|
||||
- Do not mark as universal wheel because Python 2.6 is different from
|
||||
other builds due to the extra argparse dependency. Fixes issue566.
|
||||
Thanks sontek.
|
||||
|
||||
52
doc/en/announce/release-2.6.3.txt
Normal file
52
doc/en/announce/release-2.6.3.txt
Normal file
@@ -0,0 +1,52 @@
|
||||
pytest-2.6.3: fixes and little improvements
|
||||
===========================================================================
|
||||
|
||||
pytest is a mature Python testing tool with more than a 1100 tests
|
||||
against itself, passing on many different interpreters and platforms.
|
||||
This release is drop-in compatible to 2.5.2 and 2.6.X.
|
||||
See below for the changes and see docs at:
|
||||
|
||||
http://pytest.org
|
||||
|
||||
As usual, you can upgrade from pypi via::
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all who contributed, among them:
|
||||
|
||||
Floris Bruynooghe
|
||||
Oleg Sinyavskiy
|
||||
Uwe Schmitt
|
||||
Charles Cloud
|
||||
Wolfgang Schnerring
|
||||
|
||||
have fun,
|
||||
holger krekel
|
||||
|
||||
Changes 2.6.3
|
||||
======================
|
||||
|
||||
- fix issue575: xunit-xml was reporting collection errors as failures
|
||||
instead of errors, thanks Oleg Sinyavskiy.
|
||||
|
||||
- fix issue582: fix setuptools example, thanks Laszlo Papp and Ronny
|
||||
Pfannschmidt.
|
||||
|
||||
- Fix infinite recursion bug when pickling capture.EncodedFile, thanks
|
||||
Uwe Schmitt.
|
||||
|
||||
- fix issue589: fix bad interaction with numpy and others when showing
|
||||
exceptions. Check for precise "maximum recursion depth exceed" exception
|
||||
instead of presuming any RuntimeError is that one (implemented in py
|
||||
dep). Thanks Charles Cloud for analysing the issue.
|
||||
|
||||
- fix conftest related fixture visibility issue: when running with a
|
||||
CWD outside a test package pytest would get fixture discovery wrong.
|
||||
Thanks to Wolfgang Schnerring for figuring out a reproducable example.
|
||||
|
||||
- Introduce pytest_enter_pdb hook (needed e.g. by pytest_timeout to cancel the
|
||||
timeout when interactively entering pdb). Thanks Wolfgang Schnerring.
|
||||
|
||||
- check xfail/skip also with non-python function test items. Thanks
|
||||
Floris Bruynooghe.
|
||||
|
||||
101
doc/en/announce/release-2.7.0.txt
Normal file
101
doc/en/announce/release-2.7.0.txt
Normal file
@@ -0,0 +1,101 @@
|
||||
pytest-2.7.0: fixes, features, speed improvements
|
||||
===========================================================================
|
||||
|
||||
pytest is a mature Python testing tool with more than a 1100 tests
|
||||
against itself, passing on many different interpreters and platforms.
|
||||
This release is supposed to be drop-in compatible to 2.6.X.
|
||||
|
||||
See below for the changes and see docs at:
|
||||
|
||||
http://pytest.org
|
||||
|
||||
As usual, you can upgrade from pypi via::
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all who contributed, among them:
|
||||
|
||||
Anatoly Bubenkoff
|
||||
Floris Bruynooghe
|
||||
Brianna Laugher
|
||||
Eric Siegerman
|
||||
Daniel Hahler
|
||||
Charles Cloud
|
||||
Tom Viner
|
||||
Holger Peters
|
||||
Ldiary Translations
|
||||
almarklein
|
||||
|
||||
have fun,
|
||||
holger krekel
|
||||
|
||||
2.7.0 (compared to 2.6.4)
|
||||
-----------------------------
|
||||
|
||||
- fix issue435: make reload() work when assert rewriting is active.
|
||||
Thanks Daniel Hahler.
|
||||
|
||||
- fix issue616: conftest.py files and their contained fixutres are now
|
||||
properly considered for visibility, independently from the exact
|
||||
current working directory and test arguments that are used.
|
||||
Many thanks to Eric Siegerman and his PR235 which contains
|
||||
systematic tests for conftest visibility and now passes.
|
||||
This change also introduces the concept of a ``rootdir`` which
|
||||
is printed as a new pytest header and documented in the pytest
|
||||
customize web page.
|
||||
|
||||
- change reporting of "diverted" tests, i.e. tests that are collected
|
||||
in one file but actually come from another (e.g. when tests in a test class
|
||||
come from a base class in a different file). We now show the nodeid
|
||||
and indicate via a postfix the other file.
|
||||
|
||||
- add ability to set command line options by environment variable PYTEST_ADDOPTS.
|
||||
|
||||
- added documentation on the new pytest-dev teams on bitbucket and
|
||||
github. See https://pytest.org/latest/contributing.html .
|
||||
Thanks to Anatoly for pushing and initial work on this.
|
||||
|
||||
- fix issue650: new option ``--docttest-ignore-import-errors`` which
|
||||
will turn import errors in doctests into skips. Thanks Charles Cloud
|
||||
for the complete PR.
|
||||
|
||||
- fix issue655: work around different ways that cause python2/3
|
||||
to leak sys.exc_info into fixtures/tests causing failures in 3rd party code
|
||||
|
||||
- fix issue615: assertion re-writing did not correctly escape % signs
|
||||
when formatting boolean operations, which tripped over mixing
|
||||
booleans with modulo operators. Thanks to Tom Viner for the report,
|
||||
triaging and fix.
|
||||
|
||||
- implement issue351: add ability to specify parametrize ids as a callable
|
||||
to generate custom test ids. Thanks Brianna Laugher for the idea and
|
||||
implementation.
|
||||
|
||||
- introduce and document new hookwrapper mechanism useful for plugins
|
||||
which want to wrap the execution of certain hooks for their purposes.
|
||||
This supersedes the undocumented ``__multicall__`` protocol which
|
||||
pytest itself and some external plugins use. Note that pytest-2.8
|
||||
is scheduled to drop supporting the old ``__multicall__``
|
||||
and only support the hookwrapper protocol.
|
||||
|
||||
- majorly speed up invocation of plugin hooks
|
||||
|
||||
- use hookwrapper mechanism in builtin pytest plugins.
|
||||
|
||||
- add a doctest ini option for doctest flags, thanks Holger Peters.
|
||||
|
||||
- add note to docs that if you want to mark a parameter and the
|
||||
parameter is a callable, you also need to pass in a reason to disambiguate
|
||||
it from the "decorator" case. Thanks Tom Viner.
|
||||
|
||||
- "python_classes" and "python_functions" options now support glob-patterns
|
||||
for test discovery, as discussed in issue600. Thanks Ldiary Translations.
|
||||
|
||||
- allow to override parametrized fixtures with non-parametrized ones and vice versa (bubenkoff).
|
||||
|
||||
- fix issue463: raise specific error for 'parameterize' misspelling (pfctdayelise).
|
||||
|
||||
- On failure, the ``sys.last_value``, ``sys.last_type`` and
|
||||
``sys.last_traceback`` are set, so that a user can inspect the error
|
||||
via postmortem debugging (almarklein).
|
||||
|
||||
58
doc/en/announce/release-2.7.1.txt
Normal file
58
doc/en/announce/release-2.7.1.txt
Normal file
@@ -0,0 +1,58 @@
|
||||
pytest-2.7.1: bug fixes
|
||||
=======================
|
||||
|
||||
pytest is a mature Python testing tool with more than a 1100 tests
|
||||
against itself, passing on many different interpreters and platforms.
|
||||
This release is supposed to be drop-in compatible to 2.7.0.
|
||||
|
||||
See below for the changes and see docs at:
|
||||
|
||||
http://pytest.org
|
||||
|
||||
As usual, you can upgrade from pypi via::
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
Bruno Oliveira
|
||||
Holger Krekel
|
||||
Ionel Maries Cristian
|
||||
Floris Bruynooghe
|
||||
|
||||
Happy testing,
|
||||
The py.test Development Team
|
||||
|
||||
|
||||
2.7.1 (compared to 2.7.0)
|
||||
-------------------------
|
||||
|
||||
- fix issue731: do not get confused by the braces which may be present
|
||||
and unbalanced in an object's repr while collapsing False
|
||||
explanations. Thanks Carl Meyer for the report and test case.
|
||||
|
||||
- fix issue553: properly handling inspect.getsourcelines failures in
|
||||
FixtureLookupError which would lead to to an internal error,
|
||||
obfuscating the original problem. Thanks talljosh for initial
|
||||
diagnose/patch and Bruno Oliveira for final patch.
|
||||
|
||||
- fix issue660: properly report scope-mismatch-access errors
|
||||
independently from ordering of fixture arguments. Also
|
||||
avoid the pytest internal traceback which does not provide
|
||||
information to the user. Thanks Holger Krekel.
|
||||
|
||||
- streamlined and documented release process. Also all versions
|
||||
(in setup.py and documentation generation) are now read
|
||||
from _pytest/__init__.py. Thanks Holger Krekel.
|
||||
|
||||
- fixed docs to remove the notion that yield-fixtures are experimental.
|
||||
They are here to stay :) Thanks Bruno Oliveira.
|
||||
|
||||
- Support building wheels by using environment markers for the
|
||||
requirements. Thanks Ionel Maries Cristian.
|
||||
|
||||
- fixed regression to 2.6.4 which surfaced e.g. in lost stdout capture printing
|
||||
when tests raised SystemExit. Thanks Holger Krekel.
|
||||
|
||||
- reintroduced _pytest fixture of the pytester plugin which is used
|
||||
at least by pytest-xdist.
|
||||
58
doc/en/announce/release-2.7.2.txt
Normal file
58
doc/en/announce/release-2.7.2.txt
Normal file
@@ -0,0 +1,58 @@
|
||||
pytest-2.7.2: bug fixes
|
||||
=======================
|
||||
|
||||
pytest is a mature Python testing tool with more than a 1100 tests
|
||||
against itself, passing on many different interpreters and platforms.
|
||||
This release is supposed to be drop-in compatible to 2.7.1.
|
||||
|
||||
See below for the changes and see docs at:
|
||||
|
||||
http://pytest.org
|
||||
|
||||
As usual, you can upgrade from pypi via::
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
Bruno Oliveira
|
||||
Floris Bruynooghe
|
||||
Punyashloka Biswal
|
||||
Aron Curzon
|
||||
Benjamin Peterson
|
||||
Thomas De Schampheleire
|
||||
Edison Gustavo Muenz
|
||||
Holger Krekel
|
||||
|
||||
Happy testing,
|
||||
The py.test Development Team
|
||||
|
||||
|
||||
2.7.2 (compared to 2.7.1)
|
||||
-----------------------------
|
||||
|
||||
- fix issue767: pytest.raises value attribute does not contain the exception
|
||||
instance on Python 2.6. Thanks Eric Siegerman for providing the test
|
||||
case and Bruno Oliveira for PR.
|
||||
|
||||
- Automatically create directory for junitxml and results log.
|
||||
Thanks Aron Curzon.
|
||||
|
||||
- fix issue713: JUnit XML reports for doctest failures.
|
||||
Thanks Punyashloka Biswal.
|
||||
|
||||
- fix issue735: assertion failures on debug versions of Python 3.4+
|
||||
Thanks Benjamin Peterson.
|
||||
|
||||
- fix issue114: skipif marker reports to internal skipping plugin;
|
||||
Thanks Floris Bruynooghe for reporting and Bruno Oliveira for the PR.
|
||||
|
||||
- fix issue748: unittest.SkipTest reports to internal pytest unittest plugin.
|
||||
Thanks Thomas De Schampheleire for reporting and Bruno Oliveira for the PR.
|
||||
|
||||
- fix issue718: failed to create representation of sets containing unsortable
|
||||
elements in python 2. Thanks Edison Gustavo Muenz
|
||||
|
||||
- fix issue756, fix issue752 (and similar issues): depend on py-1.4.29
|
||||
which has a refined algorithm for traceback generation.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
.. _apiref:
|
||||
|
||||
py.test reference documentation
|
||||
pytest reference documentation
|
||||
================================================
|
||||
|
||||
.. toctree::
|
||||
|
||||
@@ -10,7 +10,7 @@ The writing and reporting of assertions in tests
|
||||
Asserting with the ``assert`` statement
|
||||
---------------------------------------------------------
|
||||
|
||||
``py.test`` allows you to use the standard python ``assert`` for verifying
|
||||
``pytest`` allows you to use the standard python ``assert`` for verifying
|
||||
expectations and values in Python tests. For example, you can write the
|
||||
following::
|
||||
|
||||
@@ -26,7 +26,8 @@ you will see the return value of the function call::
|
||||
|
||||
$ py.test test_assert1.py
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-87, inifile:
|
||||
collected 1 items
|
||||
|
||||
test_assert1.py F
|
||||
@@ -42,7 +43,7 @@ you will see the return value of the function call::
|
||||
test_assert1.py:5: AssertionError
|
||||
========================= 1 failed in 0.01 seconds =========================
|
||||
|
||||
py.test has support for showing the values of the most common subexpressions
|
||||
``pytest`` has support for showing the values of the most common subexpressions
|
||||
including calls, attributes, comparisons, and binary and unary
|
||||
operators. (See :ref:`tbreportdemo`). This allows you to use the
|
||||
idiomatic python constructs without boilerplate code while not losing
|
||||
@@ -66,20 +67,23 @@ In order to write assertions about raised exceptions, you can use
|
||||
``pytest.raises`` as a context manager like this::
|
||||
|
||||
import pytest
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
1 / 0
|
||||
|
||||
def test_zero_division():
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
1 / 0
|
||||
|
||||
and if you need to have access to the actual exception info you may use::
|
||||
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
def f():
|
||||
def test_recursion_depth():
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
def f():
|
||||
f()
|
||||
f()
|
||||
f()
|
||||
|
||||
# do checks related to excinfo.type, excinfo.value, excinfo.traceback
|
||||
assert 'maximum recursion' in str(excinfo.value)
|
||||
|
||||
``excinfo`` is a `py.code.ExceptionInfo`_ instance, which is a wrapper around
|
||||
the actual exception raised.
|
||||
the actual exception raised. The main attributes of interest are
|
||||
``.type``, ``.value`` and ``.traceback``.
|
||||
|
||||
.. _py.code.ExceptionInfo:
|
||||
http://pylib.readthedocs.org/en/latest/code.html#py-code-exceptioninfo
|
||||
@@ -95,6 +99,22 @@ asserts that the given ``ExpectedException`` is raised. The reporter will
|
||||
provide you with helpful output in case of failures such as *no
|
||||
exception* or *wrong exception*.
|
||||
|
||||
Note that it is also possible to specify a "raises" argument to
|
||||
``pytest.mark.xfail``, which checks that the test is failing in a more
|
||||
specific way than just having any exception raised::
|
||||
|
||||
@pytest.mark.xfail(raises=IndexError)
|
||||
def test_f():
|
||||
f()
|
||||
|
||||
Using ``pytest.raises`` is likely to be better for cases where you are testing
|
||||
exceptions your own code is deliberately raising, whereas using
|
||||
``@pytest.mark.xfail`` with a check function is probably better for something
|
||||
like documenting unfixed bugs (where the test describes what "should" happen)
|
||||
or bugs in dependencies.
|
||||
|
||||
|
||||
|
||||
.. _newreport:
|
||||
|
||||
Making use of context-sensitive comparisons
|
||||
@@ -102,7 +122,7 @@ Making use of context-sensitive comparisons
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
py.test has rich support for providing context-sensitive information
|
||||
``pytest`` has rich support for providing context-sensitive information
|
||||
when it encounters comparisons. For example::
|
||||
|
||||
# content of test_assert2.py
|
||||
@@ -116,7 +136,8 @@ if you run this module::
|
||||
|
||||
$ py.test test_assert2.py
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-87, inifile:
|
||||
collected 1 items
|
||||
|
||||
test_assert2.py F
|
||||
@@ -133,6 +154,7 @@ if you run this module::
|
||||
E '1'
|
||||
E Extra items in the right set:
|
||||
E '5'
|
||||
E Use -v to get the full diff
|
||||
|
||||
test_assert2.py:5: AssertionError
|
||||
========================= 1 failed in 0.01 seconds =========================
|
||||
@@ -175,7 +197,7 @@ now, given this test module::
|
||||
f2 = Foo(2)
|
||||
assert f1 == f2
|
||||
|
||||
you can run the test module and get the custom output defined in
|
||||
you can run the test module and get the custom output defined in
|
||||
the conftest file::
|
||||
|
||||
$ py.test -q test_foocompare.py
|
||||
@@ -205,33 +227,33 @@ Advanced assertion introspection
|
||||
Reporting details about a failing assertion is achieved either by rewriting
|
||||
assert statements before they are run or re-evaluating the assert expression and
|
||||
recording the intermediate values. Which technique is used depends on the
|
||||
location of the assert, py.test's configuration, and Python version being used
|
||||
to run py.test. Note that for assert statements with a manually provided
|
||||
location of the assert, ``pytest`` configuration, and Python version being used
|
||||
to run ``pytest``. Note that for assert statements with a manually provided
|
||||
message, i.e. ``assert expr, message``, no assertion introspection takes place
|
||||
and the manually provided message will be rendered in tracebacks.
|
||||
|
||||
By default, if the Python version is greater than or equal to 2.6, py.test
|
||||
By default, if the Python version is greater than or equal to 2.6, ``pytest``
|
||||
rewrites assert statements in test modules. Rewritten assert statements put
|
||||
introspection information into the assertion failure message. py.test only
|
||||
introspection information into the assertion failure message. ``pytest`` only
|
||||
rewrites test modules directly discovered by its test collection process, so
|
||||
asserts in supporting modules which are not themselves test modules will not be
|
||||
rewritten.
|
||||
|
||||
.. note::
|
||||
|
||||
py.test rewrites test modules on import. It does this by using an import hook
|
||||
to write a new pyc files. Most of the time this works transparently. However,
|
||||
if you are messing with import yourself, the import hook may interfere. If
|
||||
this is the case, simply use ``--assert=reinterp`` or
|
||||
``pytest`` rewrites test modules on import. It does this by using an import
|
||||
hook to write a new pyc files. Most of the time this works transparently.
|
||||
However, if you are messing with import yourself, the import hook may
|
||||
interfere. If this is the case, simply use ``--assert=reinterp`` or
|
||||
``--assert=plain``. Additionally, rewriting will fail silently if it cannot
|
||||
write new pycs, i.e. in a read-only filesystem or a zipfile.
|
||||
|
||||
If an assert statement has not been rewritten or the Python version is less than
|
||||
2.6, py.test falls back on assert reinterpretation. In assert reinterpretation,
|
||||
py.test walks the frame of the function containing the assert statement to
|
||||
discover sub-expression results of the failing assert statement. You can force
|
||||
py.test to always use assertion reinterpretation by passing the
|
||||
``--assert=reinterp`` option.
|
||||
2.6, ``pytest`` falls back on assert reinterpretation. In assert
|
||||
reinterpretation, ``pytest`` walks the frame of the function containing the
|
||||
assert statement to discover sub-expression results of the failing assert
|
||||
statement. You can force ``pytest`` to always use assertion reinterpretation by
|
||||
passing the ``--assert=reinterp`` option.
|
||||
|
||||
Assert reinterpretation has a caveat not present with assert rewriting: If
|
||||
evaluating the assert expression has side effects you may get a warning that the
|
||||
@@ -250,7 +272,7 @@ easy to rewrite the assertion and avoid any trouble::
|
||||
|
||||
All assert introspection can be turned off by passing ``--assert=plain``.
|
||||
|
||||
For further information, Benjamin Peterson wrote up `Behind the scenes of py.test's new assertion rewriting <http://pybites.blogspot.com/2011/07/behind-scenes-of-pytests-new-assertion.html>`_.
|
||||
For further information, Benjamin Peterson wrote up `Behind the scenes of pytest's new assertion rewriting <http://pybites.blogspot.com/2011/07/behind-scenes-of-pytests-new-assertion.html>`_.
|
||||
|
||||
.. versionadded:: 2.1
|
||||
Add assert rewriting as an alternate introspection technique.
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
|
||||
**Test classes, modules or whole projects can make use of
|
||||
one or more fixtures**. All required fixture functions will execute
|
||||
before a test from the specifying context executes. As You can use this
|
||||
to make tests operate from a pre-initialized directory or with
|
||||
certain environment variables or with pre-configured global application
|
||||
settings.
|
||||
|
||||
For example, the Django_ project requires database
|
||||
initialization to be able to import from and use its model objects.
|
||||
For that, the `pytest-django`_ plugin provides fixtures which your
|
||||
project can then easily depend or extend on, simply by referencing the
|
||||
name of the particular fixture.
|
||||
|
||||
Fixture functions have limited visilibity which depends on where they
|
||||
are defined. If they are defined on a test class, only its test methods
|
||||
may use it. A fixture defined in a module can only be used
|
||||
from that test module. A fixture defined in a conftest.py file
|
||||
can only be used by the tests below the directory of that file.
|
||||
Lastly, plugins can define fixtures which are available across all
|
||||
projects.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Python, Java and many other languages support a so called xUnit_ style
|
||||
for providing a fixed state, `test fixtures`_, for running tests. It
|
||||
typically involves calling a autouse function ahead and a teardown
|
||||
function after test execute. In 2005 pytest introduced a scope-specific
|
||||
model of automatically detecting and calling autouse and teardown
|
||||
functions on a per-module, class or function basis. The Python unittest
|
||||
package and nose have subsequently incorporated them. This model
|
||||
remains supported by pytest as :ref:`classic xunit`.
|
||||
|
||||
One property of xunit fixture functions is that they work implicitely
|
||||
by preparing global state or setting attributes on TestCase objects.
|
||||
By contrast, pytest provides :ref:`funcargs` which allow to
|
||||
dependency-inject application test state into test functions or
|
||||
methods as function arguments. If your application is sufficiently modular
|
||||
or if you are creating a new project, we recommend you now rather head over to
|
||||
:ref:`funcargs` instead because many pytest users agree that using this
|
||||
paradigm leads to better application and test organisation.
|
||||
|
||||
However, not all programs and frameworks work and can be tested in
|
||||
a fully modular way. They rather require preparation of global state
|
||||
like database autouse on which further fixtures like preparing application
|
||||
specific tables or wrapping tests in transactions can take place. For those
|
||||
needs, pytest-2.3 now supports new **fixture functions** which come with
|
||||
a ton of improvements over classic xunit fixture writing. Fixture functions:
|
||||
|
||||
- allow to separate different autouse concerns into multiple modular functions
|
||||
|
||||
- can receive and fully interoperate with :ref:`funcargs <resources>`,
|
||||
|
||||
- are called multiple times if its funcargs are parametrized,
|
||||
|
||||
- don't need to be defined directly in your test classes or modules,
|
||||
they can also be defined in a plugin or :ref:`conftest.py <conftest.py>` files and get called
|
||||
|
||||
- are called on a per-session, per-module, per-class or per-function basis
|
||||
by means of a simple "scope" declaration.
|
||||
|
||||
- can access the :ref:`request <request>` object which allows to
|
||||
introspect and interact with the (scoped) testcontext.
|
||||
|
||||
- can add cleanup functions which will be invoked when the last test
|
||||
of the fixture test context has finished executing.
|
||||
|
||||
All of these features are now demonstrated by little examples.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
test modules accessing a global resource
|
||||
-------------------------------------------------------
|
||||
|
||||
.. note::
|
||||
|
||||
Relying on `global state is considered bad programming practise <http://en.wikipedia.org/wiki/Global_variable>`_ but when you work with an application
|
||||
that relies on it you often have no choice.
|
||||
|
||||
If you want test modules to access a global resource,
|
||||
you can stick the resource to the module globals in
|
||||
a per-module autouse function. We use a :ref:`resource factory
|
||||
<@pytest.fixture>` to create our global resource::
|
||||
|
||||
# content of conftest.py
|
||||
import pytest
|
||||
|
||||
class GlobalResource:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def globresource():
|
||||
return GlobalResource()
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def setresource(request, globresource):
|
||||
request.module.globresource = globresource
|
||||
|
||||
Now any test module can access ``globresource`` as a module global::
|
||||
|
||||
# content of test_glob.py
|
||||
|
||||
def test_1():
|
||||
print ("test_1 %s" % globresource)
|
||||
def test_2():
|
||||
print ("test_2 %s" % globresource)
|
||||
|
||||
Let's run this module without output-capturing::
|
||||
|
||||
$ py.test -qs test_glob.py
|
||||
FF
|
||||
================================= FAILURES =================================
|
||||
__________________________________ test_1 __________________________________
|
||||
|
||||
def test_1():
|
||||
> print ("test_1 %s" % globresource)
|
||||
E NameError: global name 'globresource' is not defined
|
||||
|
||||
test_glob.py:3: NameError
|
||||
__________________________________ test_2 __________________________________
|
||||
|
||||
def test_2():
|
||||
> print ("test_2 %s" % globresource)
|
||||
E NameError: global name 'globresource' is not defined
|
||||
|
||||
test_glob.py:5: NameError
|
||||
2 failed in 0.01 seconds
|
||||
|
||||
The two tests see the same global ``globresource`` object.
|
||||
|
||||
Parametrizing the global resource
|
||||
+++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
We extend the previous example and add parametrization to the globresource
|
||||
factory and also add a finalizer::
|
||||
|
||||
# content of conftest.py
|
||||
|
||||
import pytest
|
||||
|
||||
class GlobalResource:
|
||||
def __init__(self, param):
|
||||
self.param = param
|
||||
|
||||
@pytest.fixture(scope="session", params=[1,2])
|
||||
def globresource(request):
|
||||
g = GlobalResource(request.param)
|
||||
def fin():
|
||||
print "finalizing", g
|
||||
request.addfinalizer(fin)
|
||||
return g
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def setresource(request, globresource):
|
||||
request.module.globresource = globresource
|
||||
|
||||
And then re-run our test module::
|
||||
|
||||
$ py.test -qs test_glob.py
|
||||
FF
|
||||
================================= FAILURES =================================
|
||||
__________________________________ test_1 __________________________________
|
||||
|
||||
def test_1():
|
||||
> print ("test_1 %s" % globresource)
|
||||
E NameError: global name 'globresource' is not defined
|
||||
|
||||
test_glob.py:3: NameError
|
||||
__________________________________ test_2 __________________________________
|
||||
|
||||
def test_2():
|
||||
> print ("test_2 %s" % globresource)
|
||||
E NameError: global name 'globresource' is not defined
|
||||
|
||||
test_glob.py:5: NameError
|
||||
2 failed in 0.01 seconds
|
||||
|
||||
We are now running the two tests twice with two different global resource
|
||||
instances. Note that the tests are ordered such that only
|
||||
one instance is active at any given time: the finalizer of
|
||||
the first globresource instance is called before the second
|
||||
instance is created and sent to the autouse functions.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
Setting up bash completion
|
||||
==========================
|
||||
|
||||
When using bash as your shell, ``py.test`` can use argcomplete
|
||||
When using bash as your shell, ``pytest`` can use argcomplete
|
||||
(https://argcomplete.readthedocs.org/) for auto-completion.
|
||||
For this ``argcomplete`` needs to be installed **and** enabled.
|
||||
|
||||
@@ -16,11 +16,11 @@ For global activation of all argcomplete enabled python applications run::
|
||||
|
||||
sudo activate-global-python-argcomplete
|
||||
|
||||
For permanent (but not global) ``py.test`` activation, use::
|
||||
For permanent (but not global) ``pytest`` activation, use::
|
||||
|
||||
register-python-argcomplete py.test >> ~/.bashrc
|
||||
|
||||
For one-time activation of argcomplete for ``py.test`` only, use::
|
||||
For one-time activation of argcomplete for ``pytest`` only, use::
|
||||
|
||||
eval "$(register-python-argcomplete py.test)"
|
||||
|
||||
|
||||
@@ -77,12 +77,10 @@ You can ask for available builtin or project-custom
|
||||
enables capturing of writes to sys.stdout/sys.stderr and makes
|
||||
captured output available via ``capsys.readouterr()`` method calls
|
||||
which return a ``(out, err)`` tuple.
|
||||
|
||||
capfd
|
||||
enables capturing of writes to file descriptors 1 and 2 and makes
|
||||
captured output available via ``capsys.readouterr()`` method calls
|
||||
captured output available via ``capfd.readouterr()`` method calls
|
||||
which return a ``(out, err)`` tuple.
|
||||
|
||||
monkeypatch
|
||||
The returned ``monkeypatch`` funcarg provides these
|
||||
helper methods to modify objects, dictionaries or os.environ::
|
||||
@@ -100,7 +98,6 @@ You can ask for available builtin or project-custom
|
||||
test function has finished. The ``raising``
|
||||
parameter determines if a KeyError or AttributeError
|
||||
will be raised if the set/deletion operation has no target.
|
||||
|
||||
pytestconfig
|
||||
the pytest config object with access to command line opts.
|
||||
recwarn
|
||||
@@ -111,13 +108,11 @@ You can ask for available builtin or project-custom
|
||||
|
||||
See http://docs.python.org/library/warnings.html for information
|
||||
on warning categories.
|
||||
|
||||
tmpdir
|
||||
return a temporary directory path object
|
||||
which is unique to each test function invocation,
|
||||
created as a sub directory of the base temporary
|
||||
directory. The returned object is a `py.path.local`_
|
||||
path object.
|
||||
|
||||
|
||||
in 0.00 seconds
|
||||
|
||||
@@ -23,7 +23,7 @@ a test.
|
||||
Setting capturing methods or disabling capturing
|
||||
-------------------------------------------------
|
||||
|
||||
There are two ways in which ``py.test`` can perform capturing:
|
||||
There are two ways in which ``pytest`` can perform capturing:
|
||||
|
||||
* file descriptor (FD) level capturing (default): All writes going to the
|
||||
operating system file descriptors 1 and 2 will be captured.
|
||||
@@ -49,7 +49,7 @@ One primary benefit of the default capturing of stdout/stderr output
|
||||
is that you can use print statements for debugging::
|
||||
|
||||
# content of test_module.py
|
||||
|
||||
|
||||
def setup_function(function):
|
||||
print ("setting up %s" % function)
|
||||
|
||||
@@ -64,7 +64,8 @@ of the failing function and hide the other one::
|
||||
|
||||
$ py.test
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-90, inifile:
|
||||
collected 2 items
|
||||
|
||||
test_module.py .F
|
||||
@@ -77,18 +78,16 @@ of the failing function and hide the other one::
|
||||
E assert False
|
||||
|
||||
test_module.py:9: AssertionError
|
||||
----------------------------- Captured stdout ------------------------------
|
||||
setting up <function test_func2 at 0x282d2a8>
|
||||
-------------------------- Captured stdout setup ---------------------------
|
||||
setting up <function test_func2 at 0x7fa678d6eb70>
|
||||
==================== 1 failed, 1 passed in 0.01 seconds ====================
|
||||
|
||||
Accessing captured output from a test function
|
||||
---------------------------------------------------
|
||||
|
||||
The :ref:`funcarg mechanism` allows test function a very easy
|
||||
way to access the captured output by simply using the names
|
||||
``capsys`` or ``capfd`` in the test function signature. Here
|
||||
is an example test function that performs some output related
|
||||
checks::
|
||||
The ``capsys`` and ``capfd`` fixtures allow to access stdout/stderr
|
||||
output created during test execution. Here is an example test function
|
||||
that performs some output related checks::
|
||||
|
||||
def test_myoutput(capsys): # or use "capfd" for fd-level
|
||||
print ("hello")
|
||||
@@ -105,11 +104,13 @@ and capturing will be continued. After the test
|
||||
function finishes the original streams will
|
||||
be restored. Using ``capsys`` this way frees your
|
||||
test from having to care about setting/resetting
|
||||
output streams and also interacts well with py.test's
|
||||
output streams and also interacts well with pytest's
|
||||
own per-test capturing.
|
||||
|
||||
If you want to capture on ``fd`` level you can use
|
||||
If you want to capture on filedescriptor level you can use
|
||||
the ``capfd`` function argument which offers the exact
|
||||
same interface.
|
||||
same interface but allows to also capture output from
|
||||
libraries or subprocesses that directly write to operating
|
||||
system level output streams (FD1 and FD2).
|
||||
|
||||
.. include:: links.inc
|
||||
|
||||
@@ -17,10 +17,13 @@
|
||||
#
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
# The short X.Y version.
|
||||
version = "2.4.2"
|
||||
release = "2.4.2"
|
||||
|
||||
import sys, os
|
||||
import os, sys
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
import _getdoctarget
|
||||
|
||||
version = _getdoctarget.get_minor_version_string()
|
||||
release = _getdoctarget.get_version_string()
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
@@ -54,7 +57,7 @@ master_doc = 'contents'
|
||||
|
||||
# General information about the project.
|
||||
project = u'pytest'
|
||||
copyright = u'2012, holger krekel'
|
||||
copyright = u'2015, holger krekel and pytest-dev team'
|
||||
|
||||
|
||||
|
||||
@@ -105,14 +108,19 @@ pygments_style = 'sphinx'
|
||||
|
||||
# -- Options for HTML output ---------------------------------------------------
|
||||
|
||||
sys.path.append(os.path.abspath('_themes'))
|
||||
html_theme_path = ['_themes']
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'sphinxdoc'
|
||||
html_theme = 'flask'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
html_theme_options = {}
|
||||
html_theme_options = {
|
||||
'index_logo': None
|
||||
}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
@@ -126,12 +134,12 @@ html_short_title = "pytest-%s" % release
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
html_logo = "img/pytest1.png"
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
html_favicon = "img/pytest1favi.ico"
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
@@ -150,6 +158,23 @@ html_static_path = ['_static']
|
||||
#html_sidebars = {}
|
||||
#html_sidebars = {'index': 'indexsidebar.html'}
|
||||
|
||||
html_sidebars = {
|
||||
'index': [
|
||||
'sidebarintro.html',
|
||||
'globaltoc.html',
|
||||
'links.html',
|
||||
'sourcelink.html',
|
||||
'searchbox.html'
|
||||
],
|
||||
'**': [
|
||||
'globaltoc.html',
|
||||
'relations.html',
|
||||
'links.html',
|
||||
'sourcelink.html',
|
||||
'searchbox.html'
|
||||
]
|
||||
}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
@@ -198,12 +223,12 @@ htmlhelp_basename = 'pytestdoc'
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('contents', 'pytest.tex', u'pytest Documentation',
|
||||
u'holger krekel, http://merlinux.eu', 'manual'),
|
||||
u'holger krekel, trainer and consultant, http://merlinux.eu', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
latex_logo = 'img/pytest1.png'
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
@@ -240,7 +265,7 @@ man_pages = [
|
||||
epub_title = u'pytest'
|
||||
epub_author = u'holger krekel at merlinux eu'
|
||||
epub_publisher = u'holger krekel at merlinux eu'
|
||||
epub_copyright = u'2012, holger krekel et alii'
|
||||
epub_copyright = u'2013, holger krekel et alii'
|
||||
|
||||
# The language of the text. It defaults to the language option
|
||||
# or en if the language is not set.
|
||||
|
||||
@@ -18,6 +18,9 @@ Contact channels
|
||||
|
||||
- `pytest-commit at python.org (mailing list)`_: for commits and new issues
|
||||
|
||||
- :doc:`contribution guide <contributing>` for help on submitting pull
|
||||
requests to bitbucket (including using git via gitifyhg).
|
||||
|
||||
- #pylib on irc.freenode.net IRC channel for random questions.
|
||||
|
||||
- private mail to Holger.Krekel at gmail com if you want to communicate sensitive issues
|
||||
@@ -26,7 +29,7 @@ Contact channels
|
||||
- `merlinux.eu`_ offers pytest and tox-related professional teaching and
|
||||
consulting.
|
||||
|
||||
.. _`pytest issue tracker`: http://bitbucket.org/hpk42/pytest/issues/
|
||||
.. _`pytest issue tracker`: http://bitbucket.org/pytest-dev/pytest/issues/
|
||||
.. _`old issue tracker`: http://bitbucket.org/hpk42/py-trunk/issues/
|
||||
|
||||
.. _`merlinux.eu`: http://merlinux.eu
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
.. _toc:
|
||||
|
||||
Full pytest documentation
|
||||
@@ -14,9 +13,10 @@ Full pytest documentation
|
||||
overview
|
||||
apiref
|
||||
plugins
|
||||
plugins_index/index
|
||||
example/index
|
||||
talks
|
||||
develop
|
||||
contributing
|
||||
funcarg_compare.txt
|
||||
announce/index
|
||||
|
||||
|
||||
3
doc/en/contributing.txt
Normal file
3
doc/en/contributing.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
.. _contributing:
|
||||
|
||||
.. include:: ../../CONTRIBUTING.rst
|
||||
@@ -12,37 +12,73 @@ configurations files by using the general help option::
|
||||
This will display command line and configuration file settings
|
||||
which were registered by installed plugins.
|
||||
|
||||
.. _rootdir:
|
||||
.. _inifiles:
|
||||
|
||||
How test configuration is read from configuration INI-files
|
||||
-------------------------------------------------------------
|
||||
initialization: determining rootdir and inifile
|
||||
-----------------------------------------------
|
||||
|
||||
py.test searches for the first matching ini-style configuration file
|
||||
in the directories of command line argument and the directories above.
|
||||
It looks for file basenames in this order::
|
||||
.. versionadded:: 2.7
|
||||
|
||||
pytest determines a "rootdir" for each test run which depends on
|
||||
the command line arguments (specified test files, paths) and on
|
||||
the existence of inifiles. The determined rootdir and ini-file are
|
||||
printed as part of the pytest header. The rootdir is used for constructing
|
||||
"nodeids" during collection and may also be used by plugins to store
|
||||
project/testrun-specific information.
|
||||
|
||||
Here is the algorithm which finds the rootdir from ``args``:
|
||||
|
||||
- determine the common ancestor directory for the specified ``args``.
|
||||
|
||||
- look for ``pytest.ini``, ``tox.ini`` and ``setup.cfg`` files in the
|
||||
ancestor directory and upwards. If one is matched, it becomes the
|
||||
ini-file and its directory becomes the rootdir. An existing
|
||||
``pytest.ini`` file will always be considered a match whereas
|
||||
``tox.ini`` and ``setup.cfg`` will only match if they contain
|
||||
a ``[pytest]`` section.
|
||||
|
||||
- if no ini-file was found, look for ``setup.py`` upwards from
|
||||
the common ancestor directory to determine the ``rootdir``.
|
||||
|
||||
- if no ini-file and no ``setup.py`` was found, use the already
|
||||
determined common ancestor as root directory. This allows to
|
||||
work with pytest in structures that are not part of a package
|
||||
and don't have any particular ini-file configuration.
|
||||
|
||||
Note that options from multiple ini-files candidates are never merged,
|
||||
the first one wins (``pytest.ini`` always wins even if it does not
|
||||
contain a ``[pytest]`` section).
|
||||
|
||||
The ``config`` object will subsequently carry these attributes:
|
||||
|
||||
- ``config.rootdir``: the determined root directory, guaranteed to exist.
|
||||
|
||||
- ``config.inifile``: the determined ini-file, may be ``None``.
|
||||
|
||||
The rootdir is used a reference directory for constructing test
|
||||
addresses ("nodeids") and can be used also by plugins for storing
|
||||
per-testrun information.
|
||||
|
||||
Example::
|
||||
|
||||
py.test path/to/testdir path/other/
|
||||
|
||||
will determine the common ancestor as ``path`` and then
|
||||
check for ini-files as follows::
|
||||
|
||||
# first look for pytest.ini files
|
||||
path/pytest.ini
|
||||
path/setup.cfg # must also contain [pytest] section to match
|
||||
path/tox.ini # must also contain [pytest] section to match
|
||||
pytest.ini
|
||||
tox.ini
|
||||
setup.cfg
|
||||
... # all the way down to the root
|
||||
|
||||
Searching stops when the first ``[pytest]`` section is found in any of
|
||||
these files. There is no merging of configuration values from multiple
|
||||
files. Example::
|
||||
# now look for setup.py
|
||||
path/setup.py
|
||||
setup.py
|
||||
... # all the way down to the root
|
||||
|
||||
py.test path/to/testdir
|
||||
|
||||
will look in the following dirs for a config file::
|
||||
|
||||
path/to/testdir/pytest.ini
|
||||
path/to/testdir/tox.ini
|
||||
path/to/testdir/setup.cfg
|
||||
path/to/pytest.ini
|
||||
path/to/tox.ini
|
||||
path/to/setup.cfg
|
||||
... # up until root of filesystem
|
||||
|
||||
If argument is provided to a py.test run, the current working directory
|
||||
is used to start the search.
|
||||
|
||||
.. _`how to change command line options defaults`:
|
||||
.. _`adding default options`:
|
||||
@@ -51,7 +87,7 @@ How to change command line options defaults
|
||||
------------------------------------------------
|
||||
|
||||
It can be tedious to type the same series of command line options
|
||||
every time you use py.test . For example, if you always want to see
|
||||
every time you use ``pytest``. For example, if you always want to see
|
||||
detailed info on skipped and xfailed tests, as well as have terser "dot"
|
||||
progress output, you can write it into a configuration file::
|
||||
|
||||
@@ -60,7 +96,14 @@ progress output, you can write it into a configuration file::
|
||||
[pytest]
|
||||
addopts = -rsxX -q
|
||||
|
||||
From now on, running ``py.test`` will add the specified options.
|
||||
Alternatively, you can set a PYTEST_ADDOPTS environment variable to add command
|
||||
line options while the environment is in use::
|
||||
|
||||
export PYTEST_ADDOPTS="-rsxX -q"
|
||||
|
||||
From now on, running ``pytest`` will add the specified options.
|
||||
|
||||
|
||||
|
||||
Builtin configuration file options
|
||||
----------------------------------------------
|
||||
@@ -97,15 +140,15 @@ Builtin configuration file options
|
||||
[seq] matches any character in seq
|
||||
[!seq] matches any char not in seq
|
||||
|
||||
Default patterns are ``.* _darcs CVS {args}``. Setting a ``norecursedir``
|
||||
replaces the default. Here is an example of how to avoid
|
||||
certain directories::
|
||||
Default patterns are ``'.*', 'CVS', '_darcs', '{arch}', '*.egg'``.
|
||||
Setting a ``norecursedirs`` replaces the default. Here is an example of
|
||||
how to avoid certain directories::
|
||||
|
||||
# content of setup.cfg
|
||||
[pytest]
|
||||
norecursedirs = .svn _build tmp*
|
||||
|
||||
This would tell py.test to not look into typical subversion or
|
||||
This would tell ``pytest`` to not look into typical subversion or
|
||||
sphinx-build directories or into any ``tmp`` prefixed directory.
|
||||
|
||||
.. confval:: python_files
|
||||
@@ -115,12 +158,35 @@ Builtin configuration file options
|
||||
|
||||
.. confval:: python_classes
|
||||
|
||||
One or more name prefixes determining which test classes
|
||||
are considered as test modules.
|
||||
One or more name prefixes or glob-style patterns determining which classes
|
||||
are considered for test collection. Here is an example of how to collect
|
||||
tests from classes that end in ``Suite``::
|
||||
|
||||
# content of pytest.ini
|
||||
[pytest]
|
||||
python_classes = *Suite
|
||||
|
||||
Note that ``unittest.TestCase`` derived classes are always collected
|
||||
regardless of this option, as ``unittest``'s own collection framework is used
|
||||
to collect those tests.
|
||||
|
||||
.. confval:: python_functions
|
||||
|
||||
One or more name prefixes determining which test functions
|
||||
and methods are considered as test modules.
|
||||
One or more name prefixes or glob-patterns determining which test functions
|
||||
and methods are considered tests. Here is an example of how
|
||||
to collect test functions and methods that end in ``_test``::
|
||||
|
||||
See :ref:`change naming conventions` for examples.
|
||||
# content of pytest.ini
|
||||
[pytest]
|
||||
python_functions = *_test
|
||||
|
||||
Note that this has no effect on methods that live on a ``unittest
|
||||
.TestCase`` derived class, as ``unittest``'s own collection framework is used
|
||||
to collect those tests.
|
||||
|
||||
See :ref:`change naming conventions` for more detailed examples.
|
||||
|
||||
.. confval:: doctest_optionflags
|
||||
|
||||
One or more doctest flag names from the standard ``doctest`` module.
|
||||
:doc:`See how py.test handles doctests <doctest>`.
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
=================================================
|
||||
Feedback and contribute to py.test
|
||||
=================================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
contact.txt
|
||||
|
||||
.. _checkout:
|
||||
|
||||
Working from version control or a tarball
|
||||
=================================================
|
||||
|
||||
To follow development or start experiments, checkout the
|
||||
complete code and documentation source with mercurial_::
|
||||
|
||||
hg clone https://bitbucket.org/hpk42/pytest/
|
||||
|
||||
You can also go to the python package index and
|
||||
download and unpack a TAR file::
|
||||
|
||||
http://pypi.python.org/pypi/pytest/
|
||||
|
||||
Activating a checkout with setuptools
|
||||
--------------------------------------------
|
||||
|
||||
With a working Distribute_ or setuptools_ installation you can type::
|
||||
|
||||
python setup.py develop
|
||||
|
||||
in order to work inline with the tools and the lib of your checkout.
|
||||
|
||||
If this command complains that it could not find the required version
|
||||
of "py" then you need to use the development pypi repository::
|
||||
|
||||
python setup.py develop -i http://pypi.testrun.org
|
||||
|
||||
|
||||
.. include:: links.inc
|
||||
@@ -44,15 +44,29 @@ then you can just invoke ``py.test`` without command line options::
|
||||
|
||||
$ py.test
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-96, inifile: pytest.ini
|
||||
collected 1 items
|
||||
|
||||
mymodule.py .
|
||||
|
||||
========================= 1 passed in 0.01 seconds =========================
|
||||
========================= 1 passed in 0.06 seconds =========================
|
||||
|
||||
It is possible to use fixtures using the ``getfixture`` helper::
|
||||
|
||||
# content of example.rst
|
||||
>>> tmp = getfixture('tmpdir')
|
||||
>>> ...
|
||||
>>>
|
||||
|
||||
Also, :ref:`usefixtures` and :ref:`autouse` fixtures are supported
|
||||
when executing text doctest files.
|
||||
|
||||
The standard ``doctest`` module provides some setting flags to configure the
|
||||
strictness of doctest tests. In py.test You can enable those flags those flags
|
||||
using the configuration file. To make pytest ignore trailing whitespaces and
|
||||
ignore lengthy exception stack traces you can just write::
|
||||
|
||||
# content of pytest.ini
|
||||
[pytest]
|
||||
doctest_optionflags= NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from py.test import raises
|
||||
from pytest import raises
|
||||
import py
|
||||
|
||||
def otherfunc(a,b):
|
||||
@@ -211,3 +211,27 @@ class TestMoreErrors:
|
||||
finally:
|
||||
x = 0
|
||||
|
||||
|
||||
class TestCustomAssertMsg:
|
||||
|
||||
def test_single_line(self):
|
||||
class A:
|
||||
a = 1
|
||||
b = 2
|
||||
assert A.a == b, "A.a appears not to be b"
|
||||
|
||||
def test_multiline(self):
|
||||
class A:
|
||||
a = 1
|
||||
b = 2
|
||||
assert A.a == b, "A.a appears not to be b\n" \
|
||||
"or does not appear to be b\none of those"
|
||||
|
||||
def test_custom_repr(self):
|
||||
class JSON:
|
||||
a = 1
|
||||
def __repr__(self):
|
||||
return "This is JSON\n{\n 'foo': 'bar'\n}"
|
||||
a = JSON()
|
||||
b = 2
|
||||
assert a.a == b, a
|
||||
|
||||
@@ -9,6 +9,6 @@ def test_failure_demo_fails_properly(testdir):
|
||||
failure_demo.copy(testdir.tmpdir.join(failure_demo.basename))
|
||||
result = testdir.runpytest(target)
|
||||
result.stdout.fnmatch_lines([
|
||||
"*39 failed*"
|
||||
"*42 failed*"
|
||||
])
|
||||
assert result.ret != 0
|
||||
|
||||
@@ -21,6 +21,9 @@ You can "mark" a test function with custom metadata like this::
|
||||
pass
|
||||
def test_another():
|
||||
pass
|
||||
class TestClass:
|
||||
def test_method(self):
|
||||
pass
|
||||
|
||||
.. versionadded:: 2.2
|
||||
|
||||
@@ -28,26 +31,87 @@ You can then restrict a test run to only run tests marked with ``webtest``::
|
||||
|
||||
$ py.test -v -m webtest
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2 -- /home/hpk/p/pytest/.tox/regen/bin/python
|
||||
collecting ... collected 3 items
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1 -- /tmp/sandbox/pytest/.tox/regen/bin/python3.4
|
||||
rootdir: /tmp/doc-exec-157, inifile:
|
||||
collecting ... collected 4 items
|
||||
|
||||
test_server.py:3: test_send_http PASSED
|
||||
test_server.py::test_send_http PASSED
|
||||
|
||||
=================== 2 tests deselected by "-m 'webtest'" ===================
|
||||
================== 1 passed, 2 deselected in 0.01 seconds ==================
|
||||
=================== 3 tests deselected by "-m 'webtest'" ===================
|
||||
================== 1 passed, 3 deselected in 0.01 seconds ==================
|
||||
|
||||
Or the inverse, running all tests except the webtest ones::
|
||||
|
||||
|
||||
$ py.test -v -m "not webtest"
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2 -- /home/hpk/p/pytest/.tox/regen/bin/python
|
||||
collecting ... collected 3 items
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1 -- /tmp/sandbox/pytest/.tox/regen/bin/python3.4
|
||||
rootdir: /tmp/doc-exec-157, inifile:
|
||||
collecting ... collected 4 items
|
||||
|
||||
test_server.py:6: test_something_quick PASSED
|
||||
test_server.py:8: test_another PASSED
|
||||
test_server.py::test_something_quick PASSED
|
||||
test_server.py::test_another PASSED
|
||||
test_server.py::TestClass::test_method PASSED
|
||||
|
||||
================= 1 tests deselected by "-m 'not webtest'" =================
|
||||
================== 2 passed, 1 deselected in 0.01 seconds ==================
|
||||
================== 3 passed, 1 deselected in 0.01 seconds ==================
|
||||
|
||||
Selecting tests based on their node ID
|
||||
--------------------------------------
|
||||
|
||||
You can provide one or more :ref:`node IDs <node-id>` as positional
|
||||
arguments to select only specified tests. This makes it easy to select
|
||||
tests based on their module, class, method, or function name::
|
||||
|
||||
$ py.test -v test_server.py::TestClass::test_method
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1 -- /tmp/sandbox/pytest/.tox/regen/bin/python3.4
|
||||
rootdir: /tmp/doc-exec-157, inifile:
|
||||
collecting ... collected 5 items
|
||||
|
||||
test_server.py::TestClass::test_method PASSED
|
||||
|
||||
========================= 1 passed in 0.01 seconds =========================
|
||||
|
||||
You can also select on the class::
|
||||
|
||||
$ py.test -v test_server.py::TestClass
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1 -- /tmp/sandbox/pytest/.tox/regen/bin/python3.4
|
||||
rootdir: /tmp/doc-exec-157, inifile:
|
||||
collecting ... collected 4 items
|
||||
|
||||
test_server.py::TestClass::test_method PASSED
|
||||
|
||||
========================= 1 passed in 0.01 seconds =========================
|
||||
|
||||
Or select multiple nodes::
|
||||
|
||||
$ py.test -v test_server.py::TestClass test_server.py::test_send_http
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1 -- /tmp/sandbox/pytest/.tox/regen/bin/python3.4
|
||||
rootdir: /tmp/doc-exec-157, inifile:
|
||||
collecting ... collected 8 items
|
||||
|
||||
test_server.py::TestClass::test_method PASSED
|
||||
test_server.py::test_send_http PASSED
|
||||
|
||||
========================= 2 passed in 0.01 seconds =========================
|
||||
|
||||
.. _node-id:
|
||||
|
||||
.. note::
|
||||
|
||||
Node IDs are of the form ``module.py::class::method`` or
|
||||
``module.py::function``. Node IDs control which tests are
|
||||
collected, so ``module.py::class`` will select all test methods
|
||||
on the class. Nodes are also created for each parameter of a
|
||||
parametrized fixture or test, so selecting a parametrized test
|
||||
must include the parameter value, e.g.
|
||||
``module.py::function[param]``.
|
||||
|
||||
Node IDs for failing tests are displayed in the test summary info
|
||||
when running py.test with the ``-rf`` option. You can also
|
||||
construct Node IDs from the output of ``py.test --collectonly``.
|
||||
|
||||
Using ``-k expr`` to select tests based on their name
|
||||
-------------------------------------------------------
|
||||
@@ -61,39 +125,54 @@ select tests based on their names::
|
||||
|
||||
$ py.test -v -k http # running with the above defined example module
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2 -- /home/hpk/p/pytest/.tox/regen/bin/python
|
||||
collecting ... collected 3 items
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1 -- /tmp/sandbox/pytest/.tox/regen/bin/python3.4
|
||||
rootdir: /tmp/doc-exec-157, inifile:
|
||||
collecting ... collected 4 items
|
||||
|
||||
test_server.py:3: test_send_http PASSED
|
||||
test_server.py::test_send_http PASSED
|
||||
|
||||
====================== 2 tests deselected by '-khttp' ======================
|
||||
================== 1 passed, 2 deselected in 0.01 seconds ==================
|
||||
====================== 3 tests deselected by '-khttp' ======================
|
||||
================== 1 passed, 3 deselected in 0.01 seconds ==================
|
||||
|
||||
And you can also run all tests except the ones that match the keyword::
|
||||
|
||||
$ py.test -k "not send_http" -v
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2 -- /home/hpk/p/pytest/.tox/regen/bin/python
|
||||
collecting ... collected 3 items
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1 -- /tmp/sandbox/pytest/.tox/regen/bin/python3.4
|
||||
rootdir: /tmp/doc-exec-157, inifile:
|
||||
collecting ... collected 4 items
|
||||
|
||||
test_server.py:6: test_something_quick PASSED
|
||||
test_server.py:8: test_another PASSED
|
||||
test_server.py::test_something_quick PASSED
|
||||
test_server.py::test_another PASSED
|
||||
test_server.py::TestClass::test_method PASSED
|
||||
|
||||
================= 1 tests deselected by '-knot send_http' ==================
|
||||
================== 2 passed, 1 deselected in 0.01 seconds ==================
|
||||
================== 3 passed, 1 deselected in 0.01 seconds ==================
|
||||
|
||||
Or to select "http" and "quick" tests::
|
||||
|
||||
$ py.test -k "http or quick" -v
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2 -- /home/hpk/p/pytest/.tox/regen/bin/python
|
||||
collecting ... collected 3 items
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1 -- /tmp/sandbox/pytest/.tox/regen/bin/python3.4
|
||||
rootdir: /tmp/doc-exec-157, inifile:
|
||||
collecting ... collected 4 items
|
||||
|
||||
test_server.py:3: test_send_http PASSED
|
||||
test_server.py:6: test_something_quick PASSED
|
||||
test_server.py::test_send_http PASSED
|
||||
test_server.py::test_something_quick PASSED
|
||||
|
||||
================= 1 tests deselected by '-khttp or quick' ==================
|
||||
================== 2 passed, 1 deselected in 0.01 seconds ==================
|
||||
================= 2 tests deselected by '-khttp or quick' ==================
|
||||
================== 2 passed, 2 deselected in 0.01 seconds ==================
|
||||
|
||||
.. note::
|
||||
|
||||
If you are using expressions such as "X and Y" then both X and Y
|
||||
need to be simple non-keyword names. For example, "pass" or "from"
|
||||
will result in SyntaxErrors because "-k" evaluates the expression.
|
||||
|
||||
However, if the "-k" argument is a simple string, no such restrictions
|
||||
apply. Also "-k 'not STRING'" has no restrictions. You can also
|
||||
specify numbers like "-k 1.3" to match tests which are parametrized
|
||||
with the float "1.3".
|
||||
|
||||
Registering markers
|
||||
-------------------------------------
|
||||
@@ -116,9 +195,9 @@ You can ask which markers exist for your test suite - the list includes our just
|
||||
|
||||
@pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see http://pytest.org/latest/skipping.html
|
||||
|
||||
@pytest.mark.xfail(condition, reason=None, run=True): mark the the test function as an expected failure if eval(condition) has a True value. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. See http://pytest.org/latest/skipping.html
|
||||
@pytest.mark.xfail(condition, reason=None, run=True, raises=None): mark the the test function as an expected failure if eval(condition) has a True value. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. If only specific exception(s) are expected, you can list them in raises, and if the test fails in other ways, it will be reported as a true failure. See http://pytest.org/latest/skipping.html
|
||||
|
||||
@pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in multiple different argument value sets. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2. see http://pytest.org/latest/parametrize.html for more info and examples.
|
||||
@pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in different arguments in turn. argvalues generally needs to be a list of values if argnames specifies only one name or a list of tuples of values if argnames specifies multiple names. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2.see http://pytest.org/latest/parametrize.html for more info and examples.
|
||||
|
||||
@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures
|
||||
|
||||
@@ -139,8 +218,8 @@ For an example on how to add and work with markers from a plugin, see
|
||||
* asking for existing markers via ``py.test --markers`` gives good output
|
||||
|
||||
* typos in function markers are treated as an error if you use
|
||||
the ``--strict`` option. Later versions of py.test are probably
|
||||
going to treat non-registered markers as an error.
|
||||
the ``--strict`` option. Future versions of ``pytest`` are probably
|
||||
going to start treating non-registered markers as errors at some point.
|
||||
|
||||
.. _`scoped-marking`:
|
||||
|
||||
@@ -209,6 +288,14 @@ In this example the mark "foo" will apply to each of the three
|
||||
tests, whereas the "bar" mark is only applied to the second test.
|
||||
Skip and xfail marks can also be applied in this way, see :ref:`skip/xfail with parametrize`.
|
||||
|
||||
.. note::
|
||||
|
||||
If the data you are parametrizing happen to be single callables, you need to be careful
|
||||
when marking these items. `pytest.mark.xfail(my_func)` won't work because it's also the
|
||||
signature of a function being decorated. To resolve this ambiguity, you need to pass a
|
||||
reason argument:
|
||||
`pytest.mark.xfail(func_bar, reason="Issue#7")`.
|
||||
|
||||
|
||||
.. _`adding a custom marker from a plugin`:
|
||||
|
||||
@@ -255,18 +342,20 @@ the test needs::
|
||||
|
||||
$ py.test -E stage2
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-157, inifile:
|
||||
collected 1 items
|
||||
|
||||
test_someenv.py s
|
||||
|
||||
======================== 1 skipped in 0.01 seconds =========================
|
||||
|
||||
|
||||
and here is one that specifies exactly the environment needed::
|
||||
|
||||
$ py.test -E stage1
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-157, inifile:
|
||||
collected 1 items
|
||||
|
||||
test_someenv.py .
|
||||
@@ -280,9 +369,9 @@ The ``--markers`` option always gives you a list of available markers::
|
||||
|
||||
@pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see http://pytest.org/latest/skipping.html
|
||||
|
||||
@pytest.mark.xfail(condition, reason=None, run=True): mark the the test function as an expected failure if eval(condition) has a True value. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. See http://pytest.org/latest/skipping.html
|
||||
@pytest.mark.xfail(condition, reason=None, run=True, raises=None): mark the the test function as an expected failure if eval(condition) has a True value. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. If only specific exception(s) are expected, you can list them in raises, and if the test fails in other ways, it will be reported as a true failure. See http://pytest.org/latest/skipping.html
|
||||
|
||||
@pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in multiple different argument value sets. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2. see http://pytest.org/latest/parametrize.html for more info and examples.
|
||||
@pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in different arguments in turn. argvalues generally needs to be a list of values if argnames specifies only one name or a list of tuples of values if argnames specifies multiple names. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2.see http://pytest.org/latest/parametrize.html for more info and examples.
|
||||
|
||||
@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures
|
||||
|
||||
@@ -290,7 +379,7 @@ The ``--markers`` option always gives you a list of available markers::
|
||||
|
||||
@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible.
|
||||
|
||||
|
||||
|
||||
Reading markers which were set from multiple places
|
||||
----------------------------------------------------
|
||||
|
||||
@@ -326,7 +415,7 @@ test function. From a conftest file we can read it like this::
|
||||
|
||||
Let's run this without capturing output and see what we get::
|
||||
|
||||
$ py.test -q -s
|
||||
$ py.test -q -s
|
||||
glob args=('function',) kwargs={'x': 3}
|
||||
glob args=('class',) kwargs={'x': 2}
|
||||
glob args=('module',) kwargs={'x': 1}
|
||||
@@ -341,7 +430,7 @@ marking platform specific tests with pytest
|
||||
Consider you have a test suite which marks tests for particular platforms,
|
||||
namely ``pytest.mark.osx``, ``pytest.mark.win32`` etc. and you
|
||||
also have tests that run on all platforms and have no specific
|
||||
marker. If you now want to have a way to only run the tests
|
||||
marker. If you now want to have a way to only run the tests
|
||||
for your particular platform, you could use the following plugin::
|
||||
|
||||
# content of conftest.py
|
||||
@@ -384,28 +473,30 @@ then you will see two test skipped and two executed tests as expected::
|
||||
|
||||
$ py.test -rs # this option reports skip reasons
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-157, inifile:
|
||||
collected 4 items
|
||||
|
||||
test_plat.py s.s.
|
||||
test_plat.py sss.
|
||||
========================= short test summary info ==========================
|
||||
SKIP [2] /tmp/doc-exec-598/conftest.py:12: cannot run on platform linux2
|
||||
SKIP [3] /tmp/doc-exec-157/conftest.py:12: cannot run on platform linux
|
||||
|
||||
=================== 2 passed, 2 skipped in 0.01 seconds ====================
|
||||
=================== 1 passed, 3 skipped in 0.01 seconds ====================
|
||||
|
||||
Note that if you specify a platform via the marker-command line option like this::
|
||||
|
||||
$ py.test -m linux2
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-157, inifile:
|
||||
collected 4 items
|
||||
|
||||
test_plat.py .
|
||||
test_plat.py s
|
||||
|
||||
=================== 3 tests deselected by "-m 'linux2'" ====================
|
||||
================== 1 passed, 3 deselected in 0.01 seconds ==================
|
||||
================= 1 skipped, 3 deselected in 0.01 seconds ==================
|
||||
|
||||
then the unmarked-tests will not be run. It is thus a way to restrict the run to the specific tests.
|
||||
then the unmarked-tests will not be run. It is thus a way to restrict the run to the specific tests.
|
||||
|
||||
Automatically adding markers based on test names
|
||||
--------------------------------------------------------
|
||||
@@ -424,7 +515,7 @@ at this test module::
|
||||
|
||||
def test_interface_complex():
|
||||
assert 0
|
||||
|
||||
|
||||
def test_event_simple():
|
||||
assert 0
|
||||
|
||||
@@ -435,7 +526,7 @@ We want to dynamically define two markers and can do it in a
|
||||
``conftest.py`` plugin::
|
||||
|
||||
# content of conftest.py
|
||||
|
||||
|
||||
import pytest
|
||||
def pytest_collection_modifyitems(items):
|
||||
for item in items:
|
||||
@@ -448,7 +539,8 @@ We can now use the ``-m option`` to select one set::
|
||||
|
||||
$ py.test -m interface --tb=short
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-157, inifile:
|
||||
collected 4 items
|
||||
|
||||
test_module.py FF
|
||||
@@ -456,20 +548,21 @@ We can now use the ``-m option`` to select one set::
|
||||
================================= FAILURES =================================
|
||||
__________________________ test_interface_simple ___________________________
|
||||
test_module.py:3: in test_interface_simple
|
||||
> assert 0
|
||||
E assert 0
|
||||
assert 0
|
||||
E assert 0
|
||||
__________________________ test_interface_complex __________________________
|
||||
test_module.py:6: in test_interface_complex
|
||||
> assert 0
|
||||
E assert 0
|
||||
assert 0
|
||||
E assert 0
|
||||
================== 2 tests deselected by "-m 'interface'" ==================
|
||||
================== 2 failed, 2 deselected in 0.01 seconds ==================
|
||||
================== 2 failed, 2 deselected in 0.02 seconds ==================
|
||||
|
||||
or to select both "event" and "interface" tests::
|
||||
|
||||
$ py.test -m "interface or event" --tb=short
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-157, inifile:
|
||||
collected 4 items
|
||||
|
||||
test_module.py FFF
|
||||
@@ -477,15 +570,15 @@ or to select both "event" and "interface" tests::
|
||||
================================= FAILURES =================================
|
||||
__________________________ test_interface_simple ___________________________
|
||||
test_module.py:3: in test_interface_simple
|
||||
> assert 0
|
||||
E assert 0
|
||||
assert 0
|
||||
E assert 0
|
||||
__________________________ test_interface_complex __________________________
|
||||
test_module.py:6: in test_interface_complex
|
||||
> assert 0
|
||||
E assert 0
|
||||
assert 0
|
||||
E assert 0
|
||||
____________________________ test_event_simple _____________________________
|
||||
test_module.py:9: in test_event_simple
|
||||
> assert 0
|
||||
E assert 0
|
||||
assert 0
|
||||
E assert 0
|
||||
============= 1 tests deselected by "-m 'interface or event'" ==============
|
||||
================== 3 failed, 1 deselected in 0.02 seconds ==================
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
module containing a parametrized tests testing cross-python
|
||||
serialization via the pickle module.
|
||||
"""
|
||||
import py, pytest
|
||||
import py
|
||||
import pytest
|
||||
|
||||
pythonlist = ['python2.4', 'python2.5', 'python2.6', 'python2.7', 'python2.8']
|
||||
pythonlist = ['python2.6', 'python2.7', 'python3.3']
|
||||
@pytest.fixture(params=pythonlist)
|
||||
def python1(request, tmpdir):
|
||||
picklefile = tmpdir.join("data.pickle")
|
||||
@@ -18,14 +19,14 @@ class Python:
|
||||
def __init__(self, version, picklefile):
|
||||
self.pythonpath = py.path.local.sysfind(version)
|
||||
if not self.pythonpath:
|
||||
py.test.skip("%r not found" %(version,))
|
||||
pytest.skip("%r not found" %(version,))
|
||||
self.picklefile = picklefile
|
||||
def dumps(self, obj):
|
||||
dumpfile = self.picklefile.dirpath("dump.py")
|
||||
dumpfile.write(py.code.Source("""
|
||||
import pickle
|
||||
f = open(%r, 'wb')
|
||||
s = pickle.dump(%r, f)
|
||||
s = pickle.dump(%r, f, protocol=2)
|
||||
f.close()
|
||||
""" % (str(self.picklefile), obj)))
|
||||
py.process.cmdexec("%s %s" %(self.pythonpath, dumpfile))
|
||||
|
||||
@@ -27,7 +27,8 @@ now execute the test specification::
|
||||
|
||||
nonpython $ py.test test_simple.yml
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/sandbox/pytest/doc/en, inifile: pytest.ini
|
||||
collected 2 items
|
||||
|
||||
test_simple.yml .F
|
||||
@@ -37,7 +38,7 @@ now execute the test specification::
|
||||
usecase execution failed
|
||||
spec failed: 'some': 'other'
|
||||
no further details known at this point.
|
||||
==================== 1 failed, 1 passed in 0.03 seconds ====================
|
||||
==================== 1 failed, 1 passed in 0.19 seconds ====================
|
||||
|
||||
You get one dot for the passing ``sub1: sub1`` check and one failure.
|
||||
Obviously in the above ``conftest.py`` you'll want to implement a more
|
||||
@@ -56,28 +57,30 @@ consulted when reporting in ``verbose`` mode::
|
||||
|
||||
nonpython $ py.test -v
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2 -- /home/hpk/p/pytest/.tox/regen/bin/python
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1 -- /tmp/sandbox/pytest/.tox/regen/bin/python3.4
|
||||
rootdir: /tmp/sandbox/pytest/doc/en, inifile: pytest.ini
|
||||
collecting ... collected 2 items
|
||||
|
||||
test_simple.yml:1: usecase: ok PASSED
|
||||
test_simple.yml:1: usecase: hello FAILED
|
||||
test_simple.yml::ok PASSED
|
||||
test_simple.yml::hello FAILED
|
||||
|
||||
================================= FAILURES =================================
|
||||
______________________________ usecase: hello ______________________________
|
||||
usecase execution failed
|
||||
spec failed: 'some': 'other'
|
||||
no further details known at this point.
|
||||
==================== 1 failed, 1 passed in 0.03 seconds ====================
|
||||
==================== 1 failed, 1 passed in 0.05 seconds ====================
|
||||
|
||||
While developing your custom test collection and execution it's also
|
||||
interesting to just look at the collection tree::
|
||||
|
||||
nonpython $ py.test --collect-only
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/sandbox/pytest/doc/en, inifile: pytest.ini
|
||||
collected 2 items
|
||||
<YamlFile 'test_simple.yml'>
|
||||
<YamlFile 'example/nonpython/test_simple.yml'>
|
||||
<YamlItem 'ok'>
|
||||
<YamlItem 'hello'>
|
||||
|
||||
============================= in 0.03 seconds =============================
|
||||
============================= in 0.04 seconds =============================
|
||||
|
||||
@@ -6,7 +6,7 @@ Parametrizing tests
|
||||
|
||||
.. currentmodule:: _pytest.python
|
||||
|
||||
py.test allows to easily parametrize test functions.
|
||||
``pytest`` allows to easily parametrize test functions.
|
||||
For basic docs, see :ref:`parametrize-basics`.
|
||||
|
||||
In the following we provide some examples using
|
||||
@@ -63,11 +63,76 @@ let's run the full monty::
|
||||
E assert 4 < 4
|
||||
|
||||
test_compute.py:3: AssertionError
|
||||
1 failed, 4 passed in 0.01 seconds
|
||||
1 failed, 4 passed in 0.02 seconds
|
||||
|
||||
As expected when running the full range of ``param1`` values
|
||||
we'll get an error on the last one.
|
||||
|
||||
|
||||
Different options for test IDs
|
||||
------------------------------------
|
||||
|
||||
pytest will build a string that is the test ID for each set of values in a
|
||||
parametrized test. These IDs can be used with ``-k`` to select specific cases
|
||||
to run, and they will also identify the specific case when one is failing.
|
||||
Running pytest with ``--collect-only`` will show the generated IDs.
|
||||
|
||||
Numbers, strings, booleans and None will have their usual string representation
|
||||
used in the test ID. For other objects, pytest will make a string based on
|
||||
the argument name::
|
||||
|
||||
# contents of test_time.py
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
testdata = [(datetime(2001, 12, 12), datetime(2001, 12, 11), timedelta(1)),
|
||||
(datetime(2001, 12, 11), datetime(2001, 12, 12), timedelta(-1)),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("a,b,expected", testdata)
|
||||
def test_timedistance_v0(a, b, expected):
|
||||
diff = a - b
|
||||
assert diff == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("a,b,expected", testdata, ids=["forward", "backward"])
|
||||
def test_timedistance_v1(a, b, expected):
|
||||
diff = a - b
|
||||
assert diff == expected
|
||||
|
||||
|
||||
def idfn(val):
|
||||
if isinstance(val, (datetime,)):
|
||||
# note this wouldn't show any hours/minutes/seconds
|
||||
return val.strftime('%Y%m%d')
|
||||
|
||||
|
||||
@pytest.mark.parametrize("a,b,expected", testdata, ids=idfn)
|
||||
def test_timedistance_v2(a, b, expected):
|
||||
diff = a - b
|
||||
assert diff == expected
|
||||
|
||||
|
||||
In ``test_timedistance_v0``, we let pytest generate the test IDs.
|
||||
|
||||
In ``test_timedistance_v1``, we specified ``ids`` as a list of strings which were
|
||||
used as the test IDs. These are succinct, but can be a pain to maintain.
|
||||
|
||||
In ``test_timedistance_v2``, we specified ``ids`` as a function that can generate a
|
||||
string representation to make part of the test ID. So our ``datetime`` values use the
|
||||
label generated by ``idfn``, but because we didn't generate a label for ``timedelta``
|
||||
objects, they are still using the default pytest representation::
|
||||
|
||||
|
||||
$ py.test test_time.py --collect-only
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-159, inifile:
|
||||
|
||||
============================= in 0.00 seconds =============================
|
||||
ERROR: file not found: test_time.py
|
||||
|
||||
A quick port of "testscenarios"
|
||||
------------------------------------
|
||||
|
||||
@@ -106,19 +171,21 @@ this is a fully self-contained example which you can run with::
|
||||
|
||||
$ py.test test_scenarios.py
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-159, inifile:
|
||||
collected 4 items
|
||||
|
||||
test_scenarios.py ....
|
||||
|
||||
========================= 4 passed in 0.01 seconds =========================
|
||||
========================= 4 passed in 0.02 seconds =========================
|
||||
|
||||
If you just collect tests you'll also nicely see 'advanced' and 'basic' as variants for the test function::
|
||||
|
||||
|
||||
$ py.test --collect-only test_scenarios.py
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-159, inifile:
|
||||
collected 4 items
|
||||
<Module 'test_scenarios.py'>
|
||||
<Class 'TestSampleWithScenarios'>
|
||||
@@ -182,13 +249,14 @@ Let's first see how it looks like at collection time::
|
||||
|
||||
$ py.test test_backends.py --collect-only
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-159, inifile:
|
||||
collected 2 items
|
||||
<Module 'test_backends.py'>
|
||||
<Function 'test_db_initialized[d1]'>
|
||||
<Function 'test_db_initialized[d2]'>
|
||||
|
||||
============================= in 0.00 seconds =============================
|
||||
============================= in 0.01 seconds =============================
|
||||
|
||||
And then when we run the test::
|
||||
|
||||
@@ -197,7 +265,7 @@ And then when we run the test::
|
||||
================================= FAILURES =================================
|
||||
_________________________ test_db_initialized[d2] __________________________
|
||||
|
||||
db = <conftest.DB2 instance at 0x2dbd950>
|
||||
db = <conftest.DB2 object at 0x7f10a071cb38>
|
||||
|
||||
def test_db_initialized(db):
|
||||
# a dummy test
|
||||
@@ -251,9 +319,9 @@ argument sets to use for each test function. Let's run it::
|
||||
$ py.test -q
|
||||
F..
|
||||
================================= FAILURES =================================
|
||||
________________________ TestClass.test_equals[1-2] ________________________
|
||||
________________________ TestClass.test_equals[2-1] ________________________
|
||||
|
||||
self = <test_parametrize.TestClass instance at 0x258a6c8>, a = 1, b = 2
|
||||
self = <test_parametrize.TestClass object at 0x7f878094f630>, a = 1, b = 2
|
||||
|
||||
def test_equals(self, a, b):
|
||||
> assert a == b
|
||||
@@ -279,10 +347,8 @@ is to be run with different sets of arguments for its three arguments:
|
||||
Running it results in some skips if we don't have all the python interpreters installed and otherwise runs all combinations (5 interpreters times 5 interpreters times 3 objects to serialize/deserialize)::
|
||||
|
||||
. $ py.test -rs -q multipython.py
|
||||
............sss............sss............sss............ssssssssssssssssss
|
||||
========================= short test summary info ==========================
|
||||
SKIP [27] /home/hpk/p/pytest/doc/en/example/multipython.py:21: 'python2.8' not found
|
||||
48 passed, 27 skipped in 1.37 seconds
|
||||
...........................
|
||||
27 passed in 4.14 seconds
|
||||
|
||||
Indirect parametrization of optional implementations/imports
|
||||
--------------------------------------------------------------------
|
||||
@@ -290,7 +356,7 @@ Indirect parametrization of optional implementations/imports
|
||||
If you want to compare the outcomes of several implementations of a given
|
||||
API, you can write test functions that receive the already imported implementations
|
||||
and get skipped in case the implementation is not importable/available. Let's
|
||||
say we have a "base" implementation and the other (possibly optimized ones)
|
||||
say we have a "base" implementation and the other (possibly optimized ones)
|
||||
need to provide similar results::
|
||||
|
||||
# content of conftest.py
|
||||
@@ -329,12 +395,13 @@ If you run this with reporting for skips enabled::
|
||||
|
||||
$ py.test -rs test_module.py
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-159, inifile:
|
||||
collected 2 items
|
||||
|
||||
test_module.py s.
|
||||
test_module.py .s
|
||||
========================= short test summary info ==========================
|
||||
SKIP [1] /tmp/doc-exec-600/conftest.py:10: could not import 'opt2'
|
||||
SKIP [1] /tmp/doc-exec-159/conftest.py:10: could not import 'opt2'
|
||||
|
||||
=================== 1 passed, 1 skipped in 0.01 seconds ====================
|
||||
|
||||
@@ -342,13 +409,13 @@ You'll see that we don't have a ``opt2`` module and thus the second test run
|
||||
of our ``test_func1`` was skipped. A few notes:
|
||||
|
||||
- the fixture functions in the ``conftest.py`` file are "session-scoped" because we
|
||||
don't need to import more than once
|
||||
don't need to import more than once
|
||||
|
||||
- if you have multiple test functions and a skipped import, you will see
|
||||
the ``[1]`` count increasing in the report
|
||||
|
||||
- you can put :ref:`@pytest.mark.parametrize <@pytest.mark.parametrize>` style
|
||||
parametrization on the test functions to parametrize input/output
|
||||
parametrization on the test functions to parametrize input/output
|
||||
values as well.
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ You can set the :confval:`norecursedirs` option in an ini-file, for example your
|
||||
[pytest]
|
||||
norecursedirs = .svn _build tmp*
|
||||
|
||||
This would tell py.test to not recurse into typical subversion or sphinx-build directories or into any ``tmp`` prefixed directory.
|
||||
This would tell ``pytest`` to not recurse into typical subversion or sphinx-build directories or into any ``tmp`` prefixed directory.
|
||||
|
||||
.. _`change naming conventions`:
|
||||
|
||||
@@ -26,37 +26,44 @@ the :confval:`python_files`, :confval:`python_classes` and
|
||||
[pytest]
|
||||
python_files=check_*.py
|
||||
python_classes=Check
|
||||
python_functions=check
|
||||
python_functions=*_check
|
||||
|
||||
This would make py.test look for ``check_`` prefixes in
|
||||
Python filenames, ``Check`` prefixes in classes and ``check`` prefixes
|
||||
in functions and classes. For example, if we have::
|
||||
This would make ``pytest`` look for tests in files that match the ``check_*
|
||||
.py`` glob-pattern, ``Check`` prefixes in classes, and functions and methods
|
||||
that match ``*_check``. For example, if we have::
|
||||
|
||||
# content of check_myapp.py
|
||||
class CheckMyApp:
|
||||
def check_simple(self):
|
||||
def simple_check(self):
|
||||
pass
|
||||
def check_complex(self):
|
||||
def complex_check(self):
|
||||
pass
|
||||
|
||||
then the test collection looks like this::
|
||||
|
||||
$ py.test --collect-only
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-160, inifile: setup.cfg
|
||||
collected 2 items
|
||||
<Module 'check_myapp.py'>
|
||||
<Class 'CheckMyApp'>
|
||||
<Instance '()'>
|
||||
<Function 'check_simple'>
|
||||
<Function 'check_complex'>
|
||||
<Function 'simple_check'>
|
||||
<Function 'complex_check'>
|
||||
|
||||
============================= in 0.01 seconds =============================
|
||||
|
||||
.. note::
|
||||
|
||||
the ``python_functions`` and ``python_classes`` options has no effect
|
||||
for ``unittest.TestCase`` test discovery because pytest delegates
|
||||
detection of test case methods to unittest code.
|
||||
|
||||
Interpreting cmdline arguments as Python packages
|
||||
-----------------------------------------------------
|
||||
|
||||
You can use the ``--pyargs`` option to make py.test try
|
||||
You can use the ``--pyargs`` option to make ``pytest`` try
|
||||
interpreting arguments as python package names, deriving
|
||||
their file system path and then running the test. For
|
||||
example if you have unittest2 installed you can type::
|
||||
@@ -82,9 +89,10 @@ You can always peek at the collection tree without running tests like this::
|
||||
|
||||
. $ py.test --collect-only pythoncollection.py
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/sandbox/pytest/doc/en, inifile: pytest.ini
|
||||
collected 3 items
|
||||
<Module 'pythoncollection.py'>
|
||||
<Module 'example/pythoncollection.py'>
|
||||
<Function 'test_function'>
|
||||
<Class 'TestClass'>
|
||||
<Instance '()'>
|
||||
@@ -98,7 +106,7 @@ customizing test collection to find all .py files
|
||||
|
||||
.. regendoc:wipe
|
||||
|
||||
You can easily instruct py.test to discover tests from every python file::
|
||||
You can easily instruct ``pytest`` to discover tests from every python file::
|
||||
|
||||
|
||||
# content of pytest.ini
|
||||
@@ -106,8 +114,8 @@ You can easily instruct py.test to discover tests from every python file::
|
||||
python_files = *.py
|
||||
|
||||
However, many projects will have a ``setup.py`` which they don't want to be imported. Moreover, there may files only importable by a specific python version.
|
||||
For such cases you can dynamically define files to be ignored by listing
|
||||
them in a ``conftest.py`` file::
|
||||
For such cases you can dynamically define files to be ignored by listing
|
||||
them in a ``conftest.py`` file::
|
||||
|
||||
# content of conftest.py
|
||||
import sys
|
||||
@@ -130,15 +138,14 @@ and a setup.py dummy file like this::
|
||||
# content of setup.py
|
||||
0/0 # will raise exeption if imported
|
||||
|
||||
then a pytest run on python2 will find the one test when run with a python2
|
||||
then a pytest run on python2 will find the one test when run with a python2
|
||||
interpreters and will leave out the setup.py file::
|
||||
|
||||
|
||||
$ py.test --collect-only
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
collected 1 items
|
||||
<Module 'pkg/module_py2.py'>
|
||||
<Function 'test_only_on_python2'>
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-160, inifile: pytest.ini
|
||||
collected 0 items
|
||||
|
||||
============================= in 0.01 seconds =============================
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
|
||||
.. _`tbreportdemo`:
|
||||
|
||||
Demo of Python failure reports with py.test
|
||||
|
||||
Demo of Python failure reports with pytest
|
||||
==================================================
|
||||
|
||||
Here is a nice run of several tens of failures
|
||||
and how py.test presents things (unfortunately
|
||||
and how ``pytest`` presents things (unfortunately
|
||||
not showing the nice colors here in the HTML that you
|
||||
get on the terminal - we are working on that):
|
||||
|
||||
@@ -13,10 +13,11 @@ get on the terminal - we are working on that):
|
||||
|
||||
assertion $ py.test failure_demo.py
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
collected 39 items
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/sandbox/pytest/doc/en, inifile: pytest.ini
|
||||
collected 42 items
|
||||
|
||||
failure_demo.py FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
|
||||
failure_demo.py FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
|
||||
|
||||
================================= FAILURES =================================
|
||||
____________________________ test_generative[0] ____________________________
|
||||
@@ -30,7 +31,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:15: AssertionError
|
||||
_________________________ TestFailing.test_simple __________________________
|
||||
|
||||
self = <failure_demo.TestFailing object at 0x26f8f50>
|
||||
self = <failure_demo.TestFailing object at 0x7f65f1ca25c0>
|
||||
|
||||
def test_simple(self):
|
||||
def f():
|
||||
@@ -40,13 +41,13 @@ get on the terminal - we are working on that):
|
||||
|
||||
> assert f() == g()
|
||||
E assert 42 == 43
|
||||
E + where 42 = <function f at 0x269d5f0>()
|
||||
E + and 43 = <function g at 0x269d6e0>()
|
||||
E + where 42 = <function TestFailing.test_simple.<locals>.f at 0x7f65f2315510>()
|
||||
E + and 43 = <function TestFailing.test_simple.<locals>.g at 0x7f65f2323510>()
|
||||
|
||||
failure_demo.py:28: AssertionError
|
||||
____________________ TestFailing.test_simple_multiline _____________________
|
||||
|
||||
self = <failure_demo.TestFailing object at 0x26ade90>
|
||||
self = <failure_demo.TestFailing object at 0x7f65f1c812b0>
|
||||
|
||||
def test_simple_multiline(self):
|
||||
otherfunc_multi(
|
||||
@@ -66,19 +67,19 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:11: AssertionError
|
||||
___________________________ TestFailing.test_not ___________________________
|
||||
|
||||
self = <failure_demo.TestFailing object at 0x26aac10>
|
||||
self = <failure_demo.TestFailing object at 0x7f65f1c9df98>
|
||||
|
||||
def test_not(self):
|
||||
def f():
|
||||
return 42
|
||||
> assert not f()
|
||||
E assert not 42
|
||||
E + where 42 = <function f at 0x269d8c0>()
|
||||
E + where 42 = <function TestFailing.test_not.<locals>.f at 0x7f65f2323598>()
|
||||
|
||||
failure_demo.py:38: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_eq_text _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x2861490>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x7f65f1c67710>
|
||||
|
||||
def test_eq_text(self):
|
||||
> assert 'spam' == 'eggs'
|
||||
@@ -89,7 +90,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:42: AssertionError
|
||||
_____________ TestSpecialisedExplanations.test_eq_similar_text _____________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x26ade10>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x7f65f1c97198>
|
||||
|
||||
def test_eq_similar_text(self):
|
||||
> assert 'foo 1 bar' == 'foo 2 bar'
|
||||
@@ -102,7 +103,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:45: AssertionError
|
||||
____________ TestSpecialisedExplanations.test_eq_multiline_text ____________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x26f8ad0>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x7f65f1cc4d30>
|
||||
|
||||
def test_eq_multiline_text(self):
|
||||
> assert 'foo\nspam\nbar' == 'foo\neggs\nbar'
|
||||
@@ -115,7 +116,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:48: AssertionError
|
||||
______________ TestSpecialisedExplanations.test_eq_long_text _______________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x26aa450>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x7f65f1cce588>
|
||||
|
||||
def test_eq_long_text(self):
|
||||
a = '1'*100 + 'a' + '2'*100
|
||||
@@ -132,7 +133,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:53: AssertionError
|
||||
_________ TestSpecialisedExplanations.test_eq_long_text_multiline __________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x26ad7d0>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x7f65f1c81cc0>
|
||||
|
||||
def test_eq_long_text_multiline(self):
|
||||
a = '1\n'*100 + 'a' + '2\n'*100
|
||||
@@ -156,17 +157,18 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:58: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_eq_list _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x26f8550>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x7f65f1ca2cc0>
|
||||
|
||||
def test_eq_list(self):
|
||||
> assert [0, 1, 2] == [0, 1, 3]
|
||||
E assert [0, 1, 2] == [0, 1, 3]
|
||||
E At index 2 diff: 2 != 3
|
||||
E Use -v to get the full diff
|
||||
|
||||
failure_demo.py:61: AssertionError
|
||||
______________ TestSpecialisedExplanations.test_eq_list_long _______________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x26aa310>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x7f65f1c29358>
|
||||
|
||||
def test_eq_list_long(self):
|
||||
a = [0]*100 + [1] + [3]*100
|
||||
@@ -174,11 +176,12 @@ get on the terminal - we are working on that):
|
||||
> assert a == b
|
||||
E assert [0, 0, 0, 0, 0, 0, ...] == [0, 0, 0, 0, 0, 0, ...]
|
||||
E At index 100 diff: 1 != 2
|
||||
E Use -v to get the full diff
|
||||
|
||||
failure_demo.py:66: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_eq_dict _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x26a6950>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x7f65f1c9b588>
|
||||
|
||||
def test_eq_dict(self):
|
||||
> assert {'a': 0, 'b': 1, 'c': 0} == {'a': 0, 'b': 2, 'd': 0}
|
||||
@@ -190,11 +193,12 @@ get on the terminal - we are working on that):
|
||||
E {'c': 0}
|
||||
E Right contains more items:
|
||||
E {'d': 0}
|
||||
E Use -v to get the full diff
|
||||
|
||||
failure_demo.py:69: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_eq_set __________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x26e4210>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x7f65f1c7fdd8>
|
||||
|
||||
def test_eq_set(self):
|
||||
> assert set([0, 10, 11, 12]) == set([0, 20, 21])
|
||||
@@ -206,21 +210,23 @@ get on the terminal - we are working on that):
|
||||
E Extra items in the right set:
|
||||
E 20
|
||||
E 21
|
||||
E Use -v to get the full diff
|
||||
|
||||
failure_demo.py:72: AssertionError
|
||||
_____________ TestSpecialisedExplanations.test_eq_longer_list ______________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x26f9c10>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x7f65f1c347f0>
|
||||
|
||||
def test_eq_longer_list(self):
|
||||
> assert [1,2] == [1,2,3]
|
||||
E assert [1, 2] == [1, 2, 3]
|
||||
E Right contains more items, first extra item: 3
|
||||
E Use -v to get the full diff
|
||||
|
||||
failure_demo.py:75: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_in_list _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x26aac50>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x7f65f2313668>
|
||||
|
||||
def test_in_list(self):
|
||||
> assert 1 in [0, 2, 3, 4, 5]
|
||||
@@ -229,7 +235,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:78: AssertionError
|
||||
__________ TestSpecialisedExplanations.test_not_in_text_multiline __________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x26a6b90>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x7f65f1cceb38>
|
||||
|
||||
def test_not_in_text_multiline(self):
|
||||
text = 'some multiline\ntext\nwhich\nincludes foo\nand a\ntail'
|
||||
@@ -247,7 +253,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:82: AssertionError
|
||||
___________ TestSpecialisedExplanations.test_not_in_text_single ____________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x26f9d90>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x7f65f1c27438>
|
||||
|
||||
def test_not_in_text_single(self):
|
||||
text = 'single foo line'
|
||||
@@ -260,7 +266,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:86: AssertionError
|
||||
_________ TestSpecialisedExplanations.test_not_in_text_single_long _________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x26f89d0>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x7f65f1c9d4e0>
|
||||
|
||||
def test_not_in_text_single_long(self):
|
||||
text = 'head ' * 50 + 'foo ' + 'tail ' * 20
|
||||
@@ -273,7 +279,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:90: AssertionError
|
||||
______ TestSpecialisedExplanations.test_not_in_text_single_long_term _______
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x26ad310>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x7f65f1ce16d8>
|
||||
|
||||
def test_not_in_text_single_long_term(self):
|
||||
text = 'head ' * 50 + 'f'*70 + 'tail ' * 20
|
||||
@@ -292,7 +298,7 @@ get on the terminal - we are working on that):
|
||||
i = Foo()
|
||||
> assert i.b == 2
|
||||
E assert 1 == 2
|
||||
E + where 1 = <failure_demo.Foo object at 0x26e4650>.b
|
||||
E + where 1 = <failure_demo.test_attribute.<locals>.Foo object at 0x7f65f1c814e0>.b
|
||||
|
||||
failure_demo.py:101: AssertionError
|
||||
_________________________ test_attribute_instance __________________________
|
||||
@@ -302,8 +308,8 @@ get on the terminal - we are working on that):
|
||||
b = 1
|
||||
> assert Foo().b == 2
|
||||
E assert 1 == 2
|
||||
E + where 1 = <failure_demo.Foo object at 0x26f8c50>.b
|
||||
E + where <failure_demo.Foo object at 0x26f8c50> = <class 'failure_demo.Foo'>()
|
||||
E + where 1 = <failure_demo.test_attribute_instance.<locals>.Foo object at 0x7f65f1c7f7f0>.b
|
||||
E + where <failure_demo.test_attribute_instance.<locals>.Foo object at 0x7f65f1c7f7f0> = <class 'failure_demo.test_attribute_instance.<locals>.Foo'>()
|
||||
|
||||
failure_demo.py:107: AssertionError
|
||||
__________________________ test_attribute_failure __________________________
|
||||
@@ -319,7 +325,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:116:
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
|
||||
self = <failure_demo.Foo object at 0x26a65d0>
|
||||
self = <failure_demo.test_attribute_failure.<locals>.Foo object at 0x7f65f1c97dd8>
|
||||
|
||||
def _get_b(self):
|
||||
> raise Exception('Failed to get attrib')
|
||||
@@ -335,15 +341,15 @@ get on the terminal - we are working on that):
|
||||
b = 2
|
||||
> assert Foo().b == Bar().b
|
||||
E assert 1 == 2
|
||||
E + where 1 = <failure_demo.Foo object at 0x26ad050>.b
|
||||
E + where <failure_demo.Foo object at 0x26ad050> = <class 'failure_demo.Foo'>()
|
||||
E + and 2 = <failure_demo.Bar object at 0x26ad850>.b
|
||||
E + where <failure_demo.Bar object at 0x26ad850> = <class 'failure_demo.Bar'>()
|
||||
E + where 1 = <failure_demo.test_attribute_multiple.<locals>.Foo object at 0x7f65f1c9b630>.b
|
||||
E + where <failure_demo.test_attribute_multiple.<locals>.Foo object at 0x7f65f1c9b630> = <class 'failure_demo.test_attribute_multiple.<locals>.Foo'>()
|
||||
E + and 2 = <failure_demo.test_attribute_multiple.<locals>.Bar object at 0x7f65f1c9b2b0>.b
|
||||
E + where <failure_demo.test_attribute_multiple.<locals>.Bar object at 0x7f65f1c9b2b0> = <class 'failure_demo.test_attribute_multiple.<locals>.Bar'>()
|
||||
|
||||
failure_demo.py:124: AssertionError
|
||||
__________________________ TestRaises.test_raises __________________________
|
||||
|
||||
self = <failure_demo.TestRaises instance at 0x2859e18>
|
||||
self = <failure_demo.TestRaises object at 0x7f65f1c3eba8>
|
||||
|
||||
def test_raises(self):
|
||||
s = 'qwe'
|
||||
@@ -355,10 +361,10 @@ get on the terminal - we are working on that):
|
||||
> int(s)
|
||||
E ValueError: invalid literal for int() with base 10: 'qwe'
|
||||
|
||||
<0-codegen /home/hpk/p/pytest/.tox/regen/local/lib/python2.7/site-packages/_pytest/python.py:905>:1: ValueError
|
||||
<0-codegen /tmp/sandbox/pytest/.tox/regen/lib/python3.4/site-packages/_pytest/python.py:1075>:1: ValueError
|
||||
______________________ TestRaises.test_raises_doesnt _______________________
|
||||
|
||||
self = <failure_demo.TestRaises instance at 0x27013b0>
|
||||
self = <failure_demo.TestRaises object at 0x7f65f1cc4eb8>
|
||||
|
||||
def test_raises_doesnt(self):
|
||||
> raises(IOError, "int('3')")
|
||||
@@ -367,7 +373,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:136: Failed
|
||||
__________________________ TestRaises.test_raise ___________________________
|
||||
|
||||
self = <failure_demo.TestRaises instance at 0x271d9e0>
|
||||
self = <failure_demo.TestRaises object at 0x7f65f1cceeb8>
|
||||
|
||||
def test_raise(self):
|
||||
> raise ValueError("demo error")
|
||||
@@ -376,7 +382,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:139: ValueError
|
||||
________________________ TestRaises.test_tupleerror ________________________
|
||||
|
||||
self = <failure_demo.TestRaises instance at 0x270b3f8>
|
||||
self = <failure_demo.TestRaises object at 0x7f65f23136d8>
|
||||
|
||||
def test_tupleerror(self):
|
||||
> a,b = [1]
|
||||
@@ -385,7 +391,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:142: ValueError
|
||||
______ TestRaises.test_reinterpret_fails_with_print_for_the_fun_of_it ______
|
||||
|
||||
self = <failure_demo.TestRaises instance at 0x26ab368>
|
||||
self = <failure_demo.TestRaises object at 0x7f65f1ca2240>
|
||||
|
||||
def test_reinterpret_fails_with_print_for_the_fun_of_it(self):
|
||||
l = [1,2,3]
|
||||
@@ -394,15 +400,15 @@ get on the terminal - we are working on that):
|
||||
E TypeError: 'int' object is not iterable
|
||||
|
||||
failure_demo.py:147: TypeError
|
||||
----------------------------- Captured stdout ------------------------------
|
||||
--------------------------- Captured stdout call ---------------------------
|
||||
l is [1, 2, 3]
|
||||
________________________ TestRaises.test_some_error ________________________
|
||||
|
||||
self = <failure_demo.TestRaises instance at 0x271b488>
|
||||
self = <failure_demo.TestRaises object at 0x7f65f1cb36a0>
|
||||
|
||||
def test_some_error(self):
|
||||
> if namenotexi:
|
||||
E NameError: global name 'namenotexi' is not defined
|
||||
E NameError: name 'namenotexi' is not defined
|
||||
|
||||
failure_demo.py:150: NameError
|
||||
____________________ test_dynamic_compile_shows_nicely _____________________
|
||||
@@ -423,10 +429,10 @@ get on the terminal - we are working on that):
|
||||
> assert 1 == 0
|
||||
E assert 1 == 0
|
||||
|
||||
<2-codegen 'abc-123' /home/hpk/p/pytest/doc/en/example/assertion/failure_demo.py:162>:2: AssertionError
|
||||
<2-codegen 'abc-123' /tmp/sandbox/pytest/doc/en/example/assertion/failure_demo.py:162>:2: AssertionError
|
||||
____________________ TestMoreErrors.test_complex_error _____________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors instance at 0x271da28>
|
||||
self = <failure_demo.TestMoreErrors object at 0x7f65f1cb5470>
|
||||
|
||||
def test_complex_error(self):
|
||||
def f():
|
||||
@@ -437,13 +443,8 @@ get on the terminal - we are working on that):
|
||||
|
||||
failure_demo.py:175:
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
|
||||
x = 44, y = 43
|
||||
|
||||
def somefunc(x,y):
|
||||
> otherfunc(x,y)
|
||||
|
||||
failure_demo.py:8:
|
||||
failure_demo.py:8: in somefunc
|
||||
otherfunc(x,y)
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
|
||||
a = 44, b = 43
|
||||
@@ -455,7 +456,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:5: AssertionError
|
||||
___________________ TestMoreErrors.test_z1_unpack_error ____________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors instance at 0x2716950>
|
||||
self = <failure_demo.TestMoreErrors object at 0x7f65f1c9d940>
|
||||
|
||||
def test_z1_unpack_error(self):
|
||||
l = []
|
||||
@@ -465,7 +466,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:179: ValueError
|
||||
____________________ TestMoreErrors.test_z2_type_error _____________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors instance at 0x26f5e18>
|
||||
self = <failure_demo.TestMoreErrors object at 0x7f65f1c7f208>
|
||||
|
||||
def test_z2_type_error(self):
|
||||
l = 3
|
||||
@@ -475,19 +476,19 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:183: TypeError
|
||||
______________________ TestMoreErrors.test_startswith ______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors instance at 0x27075f0>
|
||||
self = <failure_demo.TestMoreErrors object at 0x7f65f1cc40b8>
|
||||
|
||||
def test_startswith(self):
|
||||
s = "123"
|
||||
g = "456"
|
||||
> assert s.startswith(g)
|
||||
E assert <built-in method startswith of str object at 0x26ff8c8>('456')
|
||||
E + where <built-in method startswith of str object at 0x26ff8c8> = '123'.startswith
|
||||
E assert <built-in method startswith of str object at 0x7f65f1ce14c8>('456')
|
||||
E + where <built-in method startswith of str object at 0x7f65f1ce14c8> = '123'.startswith
|
||||
|
||||
failure_demo.py:188: AssertionError
|
||||
__________________ TestMoreErrors.test_startswith_nested ___________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors instance at 0x2707ef0>
|
||||
self = <failure_demo.TestMoreErrors object at 0x7f65f1c81b00>
|
||||
|
||||
def test_startswith_nested(self):
|
||||
def f():
|
||||
@@ -495,15 +496,15 @@ get on the terminal - we are working on that):
|
||||
def g():
|
||||
return "456"
|
||||
> assert f().startswith(g())
|
||||
E assert <built-in method startswith of str object at 0x26ff8c8>('456')
|
||||
E + where <built-in method startswith of str object at 0x26ff8c8> = '123'.startswith
|
||||
E + where '123' = <function f at 0x269d7d0>()
|
||||
E + and '456' = <function g at 0x2698ed8>()
|
||||
E assert <built-in method startswith of str object at 0x7f65f1ce14c8>('456')
|
||||
E + where <built-in method startswith of str object at 0x7f65f1ce14c8> = '123'.startswith
|
||||
E + where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0x7f65f1c32950>()
|
||||
E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0x7f65f1c32ea0>()
|
||||
|
||||
failure_demo.py:195: AssertionError
|
||||
_____________________ TestMoreErrors.test_global_func ______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors instance at 0x271bef0>
|
||||
self = <failure_demo.TestMoreErrors object at 0x7f65f1c97240>
|
||||
|
||||
def test_global_func(self):
|
||||
> assert isinstance(globf(42), float)
|
||||
@@ -513,18 +514,18 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:198: AssertionError
|
||||
_______________________ TestMoreErrors.test_instance _______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors instance at 0x271bb90>
|
||||
self = <failure_demo.TestMoreErrors object at 0x7f65f1ce1080>
|
||||
|
||||
def test_instance(self):
|
||||
self.x = 6*7
|
||||
> assert self.x != 42
|
||||
E assert 42 != 42
|
||||
E + where 42 = <failure_demo.TestMoreErrors instance at 0x271bb90>.x
|
||||
E + where 42 = <failure_demo.TestMoreErrors object at 0x7f65f1ce1080>.x
|
||||
|
||||
failure_demo.py:202: AssertionError
|
||||
_______________________ TestMoreErrors.test_compare ________________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors instance at 0x2634170>
|
||||
self = <failure_demo.TestMoreErrors object at 0x7f65f1c3e828>
|
||||
|
||||
def test_compare(self):
|
||||
> assert globf(10) < 5
|
||||
@@ -534,7 +535,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:205: AssertionError
|
||||
_____________________ TestMoreErrors.test_try_finally ______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors instance at 0x2717f80>
|
||||
self = <failure_demo.TestMoreErrors object at 0x7f65f1c67828>
|
||||
|
||||
def test_try_finally(self):
|
||||
x = 1
|
||||
@@ -543,4 +544,55 @@ get on the terminal - we are working on that):
|
||||
E assert 1 == 0
|
||||
|
||||
failure_demo.py:210: AssertionError
|
||||
======================== 39 failed in 0.26 seconds =========================
|
||||
___________________ TestCustomAssertMsg.test_single_line ___________________
|
||||
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0x7f65f1c29860>
|
||||
|
||||
def test_single_line(self):
|
||||
class A:
|
||||
a = 1
|
||||
b = 2
|
||||
> assert A.a == b, "A.a appears not to be b"
|
||||
E AssertionError: A.a appears not to be b
|
||||
E assert 1 == 2
|
||||
E + where 1 = <class 'failure_demo.TestCustomAssertMsg.test_single_line.<locals>.A'>.a
|
||||
|
||||
failure_demo.py:221: AssertionError
|
||||
____________________ TestCustomAssertMsg.test_multiline ____________________
|
||||
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0x7f65f1c676a0>
|
||||
|
||||
def test_multiline(self):
|
||||
class A:
|
||||
a = 1
|
||||
b = 2
|
||||
> assert A.a == b, "A.a appears not to be b\n" \
|
||||
"or does not appear to be b\none of those"
|
||||
E AssertionError: A.a appears not to be b
|
||||
E or does not appear to be b
|
||||
E one of those
|
||||
E assert 1 == 2
|
||||
E + where 1 = <class 'failure_demo.TestCustomAssertMsg.test_multiline.<locals>.A'>.a
|
||||
|
||||
failure_demo.py:227: AssertionError
|
||||
___________________ TestCustomAssertMsg.test_custom_repr ___________________
|
||||
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0x7f65f1ccebe0>
|
||||
|
||||
def test_custom_repr(self):
|
||||
class JSON:
|
||||
a = 1
|
||||
def __repr__(self):
|
||||
return "This is JSON\n{\n 'foo': 'bar'\n}"
|
||||
a = JSON()
|
||||
b = 2
|
||||
> assert a.a == b, a
|
||||
E AssertionError: This is JSON
|
||||
E {
|
||||
E 'foo': 'bar'
|
||||
E }
|
||||
E assert 1 == 2
|
||||
E + where 1 = This is JSON\n{\n 'foo': 'bar'\n}.a
|
||||
|
||||
failure_demo.py:237: AssertionError
|
||||
======================== 42 failed in 0.35 seconds =========================
|
||||
|
||||
@@ -10,7 +10,7 @@ Pass different values to a test function, depending on command line options
|
||||
.. regendoc:wipe
|
||||
|
||||
Suppose we want to write a test that depends on a command line option.
|
||||
Here is a basic pattern how to achieve this::
|
||||
Here is a basic pattern to achieve this::
|
||||
|
||||
# content of test_sample.py
|
||||
def test_answer(cmdopt):
|
||||
@@ -41,9 +41,9 @@ Let's run this without supplying our new option::
|
||||
F
|
||||
================================= FAILURES =================================
|
||||
_______________________________ test_answer ________________________________
|
||||
|
||||
|
||||
cmdopt = 'type1'
|
||||
|
||||
|
||||
def test_answer(cmdopt):
|
||||
if cmdopt == "type1":
|
||||
print ("first")
|
||||
@@ -51,9 +51,9 @@ Let's run this without supplying our new option::
|
||||
print ("second")
|
||||
> assert 0 # to see what was printed
|
||||
E assert 0
|
||||
|
||||
|
||||
test_sample.py:6: AssertionError
|
||||
----------------------------- Captured stdout ------------------------------
|
||||
--------------------------- Captured stdout call ---------------------------
|
||||
first
|
||||
1 failed in 0.01 seconds
|
||||
|
||||
@@ -63,9 +63,9 @@ And now with supplying a command line option::
|
||||
F
|
||||
================================= FAILURES =================================
|
||||
_______________________________ test_answer ________________________________
|
||||
|
||||
|
||||
cmdopt = 'type2'
|
||||
|
||||
|
||||
def test_answer(cmdopt):
|
||||
if cmdopt == "type1":
|
||||
print ("first")
|
||||
@@ -73,9 +73,9 @@ And now with supplying a command line option::
|
||||
print ("second")
|
||||
> assert 0 # to see what was printed
|
||||
E assert 0
|
||||
|
||||
|
||||
test_sample.py:6: AssertionError
|
||||
----------------------------- Captured stdout ------------------------------
|
||||
--------------------------- Captured stdout call ---------------------------
|
||||
second
|
||||
1 failed in 0.01 seconds
|
||||
|
||||
@@ -108,9 +108,10 @@ directory with the above conftest.py::
|
||||
|
||||
$ py.test
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-162, inifile:
|
||||
collected 0 items
|
||||
|
||||
|
||||
============================= in 0.00 seconds =============================
|
||||
|
||||
.. _`excontrolskip`:
|
||||
@@ -152,24 +153,26 @@ and when running it will see a skipped "slow" test::
|
||||
|
||||
$ py.test -rs # "-rs" means report details on the little 's'
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-162, inifile:
|
||||
collected 2 items
|
||||
|
||||
|
||||
test_module.py .s
|
||||
========================= short test summary info ==========================
|
||||
SKIP [1] /tmp/doc-exec-603/conftest.py:9: need --runslow option to run
|
||||
|
||||
SKIP [1] /tmp/doc-exec-162/conftest.py:9: need --runslow option to run
|
||||
|
||||
=================== 1 passed, 1 skipped in 0.01 seconds ====================
|
||||
|
||||
Or run it including the ``slow`` marked test::
|
||||
|
||||
$ py.test --runslow
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-162, inifile:
|
||||
collected 2 items
|
||||
|
||||
|
||||
test_module.py ..
|
||||
|
||||
|
||||
========================= 2 passed in 0.01 seconds =========================
|
||||
|
||||
Writing well integrated assertion helpers
|
||||
@@ -193,7 +196,7 @@ Example::
|
||||
def test_something():
|
||||
checkconfig(42)
|
||||
|
||||
The ``__tracebackhide__`` setting influences py.test showing
|
||||
The ``__tracebackhide__`` setting influences ``pytest`` showing
|
||||
of tracebacks: the ``checkconfig`` function will not be shown
|
||||
unless the ``--fulltrace`` command line option is specified.
|
||||
Let's run our little function::
|
||||
@@ -202,15 +205,15 @@ Let's run our little function::
|
||||
F
|
||||
================================= FAILURES =================================
|
||||
______________________________ test_something ______________________________
|
||||
|
||||
|
||||
def test_something():
|
||||
> checkconfig(42)
|
||||
E Failed: not configured: 42
|
||||
|
||||
test_checkconfig.py:8: Failed
|
||||
1 failed in 0.01 seconds
|
||||
|
||||
Detect if running from within a py.test run
|
||||
test_checkconfig.py:8: Failed
|
||||
1 failed in 0.02 seconds
|
||||
|
||||
Detect if running from within a pytest run
|
||||
--------------------------------------------------------------
|
||||
|
||||
.. regendoc:wipe
|
||||
@@ -245,10 +248,10 @@ Adding info to test report header
|
||||
|
||||
.. regendoc:wipe
|
||||
|
||||
It's easy to present extra information in a py.test run::
|
||||
It's easy to present extra information in a ``pytest`` run::
|
||||
|
||||
# content of conftest.py
|
||||
|
||||
|
||||
def pytest_report_header(config):
|
||||
return "project deps: mylib-1.1"
|
||||
|
||||
@@ -256,10 +259,11 @@ which will add the string to the test header accordingly::
|
||||
|
||||
$ py.test
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-162, inifile:
|
||||
project deps: mylib-1.1
|
||||
collected 0 items
|
||||
|
||||
|
||||
============================= in 0.00 seconds =============================
|
||||
|
||||
.. regendoc:wipe
|
||||
@@ -279,20 +283,22 @@ which will add info only when run with "--v"::
|
||||
|
||||
$ py.test -v
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2 -- /home/hpk/p/pytest/.tox/regen/bin/python
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1 -- /tmp/sandbox/pytest/.tox/regen/bin/python3.4
|
||||
rootdir: /tmp/doc-exec-162, inifile:
|
||||
info1: did you know that ...
|
||||
did you?
|
||||
collecting ... collected 0 items
|
||||
|
||||
|
||||
============================= in 0.00 seconds =============================
|
||||
|
||||
and nothing when run plainly::
|
||||
|
||||
$ py.test
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-162, inifile:
|
||||
collected 0 items
|
||||
|
||||
|
||||
============================= in 0.00 seconds =============================
|
||||
|
||||
profiling test duration
|
||||
@@ -322,15 +328,16 @@ Now we can profile which test functions execute the slowest::
|
||||
|
||||
$ py.test --durations=3
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-162, inifile:
|
||||
collected 3 items
|
||||
|
||||
|
||||
test_some_are_slow.py ...
|
||||
|
||||
|
||||
========================= slowest 3 test durations =========================
|
||||
0.20s call test_some_are_slow.py::test_funcslow2
|
||||
0.10s call test_some_are_slow.py::test_funcslow1
|
||||
0.00s setup test_some_are_slow.py::test_funcfast
|
||||
0.00s setup test_some_are_slow.py::test_funcslow2
|
||||
========================= 3 passed in 0.31 seconds =========================
|
||||
|
||||
incremental testing - test steps
|
||||
@@ -383,20 +390,21 @@ If we run this::
|
||||
|
||||
$ py.test -rx
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-162, inifile:
|
||||
collected 4 items
|
||||
|
||||
|
||||
test_step.py .Fx.
|
||||
|
||||
|
||||
================================= FAILURES =================================
|
||||
____________________ TestUserHandling.test_modification ____________________
|
||||
|
||||
self = <test_step.TestUserHandling instance at 0x1c6fb90>
|
||||
|
||||
|
||||
self = <test_step.TestUserHandling object at 0x7ff60bbb83c8>
|
||||
|
||||
def test_modification(self):
|
||||
> assert 0
|
||||
E assert 0
|
||||
|
||||
|
||||
test_step.py:9: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
XFAIL test_step.py::TestUserHandling::()::test_deletion
|
||||
@@ -448,57 +456,60 @@ the ``db`` fixture::
|
||||
# content of b/test_error.py
|
||||
def test_root(db): # no db here, will error out
|
||||
pass
|
||||
|
||||
|
||||
We can run this::
|
||||
|
||||
$ py.test
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-162, inifile:
|
||||
collected 7 items
|
||||
|
||||
|
||||
test_step.py .Fx.
|
||||
a/test_db.py F
|
||||
a/test_db2.py F
|
||||
b/test_error.py E
|
||||
|
||||
|
||||
================================== ERRORS ==================================
|
||||
_______________________ ERROR at setup of test_root ________________________
|
||||
file /tmp/doc-exec-603/b/test_error.py, line 1
|
||||
file /tmp/doc-exec-162/b/test_error.py, line 1
|
||||
def test_root(db): # no db here, will error out
|
||||
fixture 'db' not found
|
||||
available fixtures: pytestconfig, recwarn, monkeypatch, capfd, capsys, tmpdir
|
||||
available fixtures: pytestconfig, capsys, recwarn, monkeypatch, tmpdir, capfd
|
||||
use 'py.test --fixtures [testpath]' for help on them.
|
||||
|
||||
/tmp/doc-exec-603/b/test_error.py:1
|
||||
|
||||
/tmp/doc-exec-162/b/test_error.py:1
|
||||
================================= FAILURES =================================
|
||||
____________________ TestUserHandling.test_modification ____________________
|
||||
|
||||
self = <test_step.TestUserHandling instance at 0x22f3518>
|
||||
|
||||
|
||||
self = <test_step.TestUserHandling object at 0x7f8ecd5b87f0>
|
||||
|
||||
def test_modification(self):
|
||||
> assert 0
|
||||
E assert 0
|
||||
|
||||
|
||||
test_step.py:9: AssertionError
|
||||
_________________________________ test_a1 __________________________________
|
||||
|
||||
db = <conftest.DB instance at 0x2304248>
|
||||
|
||||
|
||||
db = <conftest.DB object at 0x7f8ecdc11470>
|
||||
|
||||
def test_a1(db):
|
||||
> assert 0, db # to show value
|
||||
E AssertionError: <conftest.DB instance at 0x2304248>
|
||||
|
||||
E AssertionError: <conftest.DB object at 0x7f8ecdc11470>
|
||||
E assert 0
|
||||
|
||||
a/test_db.py:2: AssertionError
|
||||
_________________________________ test_a2 __________________________________
|
||||
|
||||
db = <conftest.DB instance at 0x2304248>
|
||||
|
||||
|
||||
db = <conftest.DB object at 0x7f8ecdc11470>
|
||||
|
||||
def test_a2(db):
|
||||
> assert 0, db # to show value
|
||||
E AssertionError: <conftest.DB instance at 0x2304248>
|
||||
|
||||
E AssertionError: <conftest.DB object at 0x7f8ecdc11470>
|
||||
E assert 0
|
||||
|
||||
a/test_db2.py:2: AssertionError
|
||||
========== 3 failed, 2 passed, 1 xfailed, 1 error in 0.03 seconds ==========
|
||||
========== 3 failed, 2 passed, 1 xfailed, 1 error in 0.05 seconds ==========
|
||||
|
||||
The two test modules in the ``a`` directory see the same ``db`` fixture instance
|
||||
while the one test in the sister-directory ``b`` doesn't see it. We could of course
|
||||
@@ -512,7 +523,7 @@ post-process test reports / failures
|
||||
---------------------------------------
|
||||
|
||||
If you want to postprocess test reports and need access to the executing
|
||||
environment you can implement a hook that gets called when the test
|
||||
environment you can implement a hook that gets called when the test
|
||||
"report" object is about to be created. Here we write out all failing
|
||||
test calls and also access a fixture (if it was used by the test) in
|
||||
case you want to query/look at it during your post processing. In our
|
||||
@@ -529,7 +540,7 @@ case we just write some informations out to a ``failures`` file::
|
||||
rep = __multicall__.execute()
|
||||
|
||||
# we only look at actual failing test calls, not setup/teardown
|
||||
if rep.when == "call" and rep.failed:
|
||||
if rep.when == "call" and rep.failed:
|
||||
mode = "a" if os.path.exists("failures") else "w"
|
||||
with open("failures", mode) as f:
|
||||
# let's also access a fixture for the fun of it
|
||||
@@ -537,7 +548,7 @@ case we just write some informations out to a ``failures`` file::
|
||||
extra = " (%s)" % item.funcargs["tmpdir"]
|
||||
else:
|
||||
extra = ""
|
||||
|
||||
|
||||
f.write(rep.nodeid + extra + "\n")
|
||||
return rep
|
||||
|
||||
@@ -545,42 +556,43 @@ if you then have failing tests::
|
||||
|
||||
# content of test_module.py
|
||||
def test_fail1(tmpdir):
|
||||
assert 0
|
||||
assert 0
|
||||
def test_fail2():
|
||||
assert 0
|
||||
|
||||
assert 0
|
||||
|
||||
and run them::
|
||||
|
||||
$ py.test test_module.py
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-162, inifile:
|
||||
collected 2 items
|
||||
|
||||
|
||||
test_module.py FF
|
||||
|
||||
|
||||
================================= FAILURES =================================
|
||||
________________________________ test_fail1 ________________________________
|
||||
|
||||
tmpdir = local('/tmp/pytest-190/test_fail10')
|
||||
|
||||
|
||||
tmpdir = local('/tmp/pytest-22/test_fail10')
|
||||
|
||||
def test_fail1(tmpdir):
|
||||
> assert 0
|
||||
E assert 0
|
||||
|
||||
|
||||
test_module.py:2: AssertionError
|
||||
________________________________ test_fail2 ________________________________
|
||||
|
||||
|
||||
def test_fail2():
|
||||
> assert 0
|
||||
E assert 0
|
||||
|
||||
|
||||
test_module.py:4: AssertionError
|
||||
========================= 2 failed in 0.01 seconds =========================
|
||||
========================= 2 failed in 0.02 seconds =========================
|
||||
|
||||
you will have a "failures" file which contains the failing test ids::
|
||||
|
||||
$ cat failures
|
||||
test_module.py::test_fail1 (/tmp/pytest-190/test_fail10)
|
||||
test_module.py::test_fail1 (/tmp/pytest-22/test_fail10)
|
||||
test_module.py::test_fail2
|
||||
|
||||
Making test result information available in fixtures
|
||||
@@ -613,71 +625,123 @@ here is a little example implemented via a local plugin::
|
||||
# request.node is an "item" because we use the default
|
||||
# "function" scope
|
||||
if request.node.rep_setup.failed:
|
||||
print "setting up a test failed!", request.node.nodeid
|
||||
print ("setting up a test failed!", request.node.nodeid)
|
||||
elif request.node.rep_setup.passed:
|
||||
if request.node.rep_call.failed:
|
||||
print "executing test failed", request.node.nodeid
|
||||
print ("executing test failed", request.node.nodeid)
|
||||
request.addfinalizer(fin)
|
||||
|
||||
|
||||
if you then have failing tests::
|
||||
|
||||
# content of test_module.py
|
||||
|
||||
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
def other():
|
||||
assert 0
|
||||
|
||||
|
||||
def test_setup_fails(something, other):
|
||||
pass
|
||||
|
||||
def test_call_fails(something):
|
||||
assert 0
|
||||
assert 0
|
||||
|
||||
def test_fail2():
|
||||
assert 0
|
||||
|
||||
assert 0
|
||||
|
||||
and run it::
|
||||
|
||||
$ py.test -s test_module.py
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.7.3 -- pytest-2.4.2
|
||||
platform linux -- Python 3.4.1 -- py-1.4.27 -- pytest-2.7.1
|
||||
rootdir: /tmp/doc-exec-162, inifile:
|
||||
collected 3 items
|
||||
|
||||
|
||||
test_module.py Esetting up a test failed! test_module.py::test_setup_fails
|
||||
Fexecuting test failed test_module.py::test_call_fails
|
||||
F
|
||||
|
||||
|
||||
================================== ERRORS ==================================
|
||||
____________________ ERROR at setup of test_setup_fails ____________________
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def other():
|
||||
> assert 0
|
||||
E assert 0
|
||||
|
||||
|
||||
test_module.py:6: AssertionError
|
||||
================================= FAILURES =================================
|
||||
_____________________________ test_call_fails ______________________________
|
||||
|
||||
|
||||
something = None
|
||||
|
||||
|
||||
def test_call_fails(something):
|
||||
> assert 0
|
||||
E assert 0
|
||||
|
||||
|
||||
test_module.py:12: AssertionError
|
||||
________________________________ test_fail2 ________________________________
|
||||
|
||||
|
||||
def test_fail2():
|
||||
> assert 0
|
||||
E assert 0
|
||||
|
||||
|
||||
test_module.py:15: AssertionError
|
||||
==================== 2 failed, 1 error in 0.02 seconds =====================
|
||||
|
||||
You'll see that the fixture finalizers could use the precise reporting
|
||||
information.
|
||||
|
||||
Integrating pytest runner and cx_freeze
|
||||
-----------------------------------------------------------
|
||||
|
||||
If you freeze your application using a tool like
|
||||
`cx_freeze <http://cx-freeze.readthedocs.org>`_ in order to distribute it
|
||||
to your end-users, it is a good idea to also package your test runner and run
|
||||
your tests using the frozen application.
|
||||
|
||||
This way packaging errors such as dependencies not being
|
||||
included into the executable can be detected early while also allowing you to
|
||||
send test files to users so they can run them in their machines, which can be
|
||||
invaluable to obtain more information about a hard to reproduce bug.
|
||||
|
||||
Unfortunately ``cx_freeze`` can't discover them
|
||||
automatically because of ``pytest``'s use of dynamic module loading, so you
|
||||
must declare them explicitly by using ``pytest.freeze_includes()``::
|
||||
|
||||
# contents of setup.py
|
||||
from cx_Freeze import setup, Executable
|
||||
import pytest
|
||||
|
||||
setup(
|
||||
name="app_main",
|
||||
executables=[Executable("app_main.py")],
|
||||
options={"build_exe":
|
||||
{
|
||||
'includes': pytest.freeze_includes()}
|
||||
},
|
||||
# ... other options
|
||||
)
|
||||
|
||||
If you don't want to ship a different executable just in order to run your tests,
|
||||
you can make your program check for a certain flag and pass control
|
||||
over to ``pytest`` instead. For example::
|
||||
|
||||
# contents of app_main.py
|
||||
import sys
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == '--pytest':
|
||||
import pytest
|
||||
sys.exit(pytest.main(sys.argv[2:]))
|
||||
else:
|
||||
# normal application execution: at this point argv can be parsed
|
||||
# by your argument-parsing library of choice as usual
|
||||
...
|
||||
|
||||
This makes it convenient to execute your tests from within your frozen
|
||||
application, using standard ``py.test`` command-line options::
|
||||
|
||||
$ ./app_main --pytest --verbose --tb=long --junit-xml=results.xml test-suite/
|
||||
/bin/sh: 1: ./app_main: not found
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
|
||||
A sesssion-fixture which can look at all collected tests
|
||||
A session-fixture which can look at all collected tests
|
||||
----------------------------------------------------------------
|
||||
|
||||
A session-scoped fixture effectively has access to all
|
||||
A session-scoped fixture effectively has access to all
|
||||
collected test items. Here is an example of a fixture
|
||||
function which walks all collected tests and looks
|
||||
if their test class defines a ``callme`` method and
|
||||
@@ -14,7 +13,7 @@ calls it::
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def callattr_ahead_of_alltests(request):
|
||||
print "callattr_ahead_of_alltests called"
|
||||
print ("callattr_ahead_of_alltests called")
|
||||
seen = set([None])
|
||||
session = request.node
|
||||
for item in session.items:
|
||||
@@ -32,35 +31,35 @@ will be called ahead of running any tests::
|
||||
class TestHello:
|
||||
@classmethod
|
||||
def callme(cls):
|
||||
print "callme called!"
|
||||
print ("callme called!")
|
||||
|
||||
def test_method1(self):
|
||||
print "test_method1 called"
|
||||
|
||||
print ("test_method1 called")
|
||||
|
||||
def test_method2(self):
|
||||
print "test_method1 called"
|
||||
print ("test_method1 called")
|
||||
|
||||
class TestOther:
|
||||
@classmethod
|
||||
def callme(cls):
|
||||
print "callme other called"
|
||||
print ("callme other called")
|
||||
def test_other(self):
|
||||
print "test other"
|
||||
print ("test other")
|
||||
|
||||
# works with unittest as well ...
|
||||
import unittest
|
||||
|
||||
|
||||
class SomeTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def callme(self):
|
||||
print "SomeTest callme called"
|
||||
print ("SomeTest callme called")
|
||||
|
||||
def test_unit1(self):
|
||||
print "test_unit1 method called"
|
||||
print ("test_unit1 method called")
|
||||
|
||||
If you run this without output capturing::
|
||||
|
||||
$ py.test -q -s test_module.py
|
||||
$ py.test -q -s test_module.py
|
||||
callattr_ahead_of_alltests called
|
||||
callme called!
|
||||
callme other called
|
||||
@@ -70,4 +69,4 @@ If you run this without output capturing::
|
||||
.test other
|
||||
.test_unit1 method called
|
||||
.
|
||||
4 passed in 0.02 seconds
|
||||
4 passed in 0.03 seconds
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user