Saturday, July 9, 2011

Behind the scenes of py.test's new assertion rewriting

py.test 2.1 was just released. py.test, which uses the Python assert statement to check test conditions, has long had support for displaying intermediate values in subexpressions of a failing assert statement. This feature is called assertion introspection. Historically, py.test performed assertion introspection by reinterpreting failed assertions in order to glean information about subexpressions. In assertion reinterpreting, py.test actually reruns the assertion noting intermediate values during interpretation. This works pretty well but is subject to several problems, most importantly that assert statements with side-effects can produce strange results because they are evaluated twice on failure. py.test 2.1's main new feature, which I wrote (with generous sponsorship from Merlinux GmbH), is a new assertion introspection technique called assertion rewriting. Assertion rewriting modifies the AST of test modules to produce subexpression information when assertions fail. This blog post will give a peek into how this is done and what the rewritten tests look like.

py.test tries to rewrite every module that it collects as a test module. Assertion rewriting uses a PEP 302 import hook to capture test modules for rewriting. I'm happy to report doing this was easier than I expected. Most of the code in the import hook I had to write was dealing with detecting test modules rather than supporting import's extremely complicated API. Rewriting has a non-zero cost during test collection, so py.test compiles rewritten modules to bytecode and caches them in the PEP 3147 PYC repository, __pycache__. One major thing I did have to account for was the possibility that multiple py.test processes would be writing PYC files. (This is a very real possibility when the xdist plugin is being used. Therefore, py.test uses only atomic operations on the rewritten PYC file. Windows, lacking atomic rename, was a pain here.

I'm now going to demonstrate what py.test's rewriting phase does to a test module. Let's dive in with a failing test for a (broken) function that is supposed to create empty files:

import os

def make_empty_file(name):
with open(name, "w") as fp:
fp.write("hello")

def test_make_empty_file():
name = "/tmp/empty_test"
make_empty_file(name)
with open(name, "r") as fp:
assert not fp.read()


This test nicely demonstrates the problem with py.test's old assertion method mentioned in the first paragraph. If we force the old assertion interpretation mode with --assert=reinterp, we see:


def test_make_empty_file():
name = "/tmp/empty_test"
make_empty_file(name)
with open(name, "r") as fp:
> assert not fp.read()
E AssertionError: (assertion failed, but when it was re-run for printing intermediate values, it did not fail. Suggestions: compute assert expression before the assert or use --no-assert)

test_empty_file.py:11: AssertionError


The problem is that assert statement has the side-effect of reading the file. When py.test reinterprets the assert statement, it uses the same file object, now at EOF, and read() returns an empty string. py.test's new rewriting mode fixes this by scanning the assert for introspection information before executing the test. Running py.test with assertion rewriting enabled gives the desired result:


def test_make_empty_file():
name = "/tmp/empty_test"
make_empty_file(name)
with open(name, "r") as fp:
> assert not fp.read()
E assert not 'hello'
E + where 'hello' = ()
E + where = .read

test_empty_file.py:11: AssertionError


So what magic has py.test worked to display such nice debugging information? This is what Python is actually executing:

def test_make_empty_file():
name = '/tmp/empty_test'
make_empty_file(name)
with open(name, 'r') as fp:
@py_assert1 = fp.read
@py_assert3 = @py_assert1()
@py_assert5 = (not @py_assert3)
if (not @py_assert5):
@py_format6 = ('assert not %(py4)s\n{%(py4)s = %(py2)s\n{%(py2)s = %(py0)s.read\n}()\n}' %
{'py0': (@pytest_ar._saferepr(fp) if ('fp' in @py_builtins.locals() is not @py_builtins.globals()) else 'fp'),
'py2': @pytest_ar._saferepr(@py_assert1),
'py4': @pytest_ar._saferepr(@py_assert3)})
raise AssertionError(@pytest_ar._format_explanation(@py_format6))
del @py_assert5, @py_assert1, @py_assert3


As you can see, it's not going to be winning any awards for beautiful Python! (Ideally, though, you'll never have to see or think about it.) Examining the rewritten code, we see a lot of internal variables starting with "@" have been created. The "@", invalid in Python identifiers, is to make sure internal names don't conflict with any user-defined names which might be in the scope. In the first four written lines under the with statement, the test of the assert statement has been expanded into its component subexpressions. This allows py.test to display the values of subexpressions should the assertion fail. If the assertion fails, the if statement in the fifth line of rewriting evaluates to True and a AssertionError will be raised. Under the if statement is the real mess. This is where the helpful error message is generated. The line starting with @py_format6 is simply does string formatting (with %) on a template generated from the structure of the assert statement. This template is filled in with the intermediate values of the expressions collected above. @py_builtins is the builtins module, used in case the test is shadowing builtins the rewriting code uses. The @pytest_ar variable is a special module of assertion formatting helpers. For example, @pytest_ar._saferepr is like builtin repr but gracefully handles long reprs and __repr__ methods that raise exceptions. A non-obvious trick in the format dict is the expression @pytest_ar._saferepr(fp) if ('fp' in @py_builtins.locals() is not @py_builtins.globals()) else 'fp'. This checks whether fp is a local variable or not and customizes the display accordingly. After the initial formatting, the helper function _format_explanation is called. This function produces the indentation and "+" you see in the error message. Finally, we note that if the assertion doesn't fail, py.test cleans up after itself by deleting temporary variables.

The example above is a fairly tame (and luckily also typical) assertion. Rewriting gets more "exciting" when boolean operations and comparisons enter because they require short circuit evaluation, which complicates both the expansion of expressions and formatting (think lots of nested ifs).

In conclusion, py.test's new assertion rewriting fixes some long standing issues with assertion introspection and continues py.test's long tradition of excellent debugging support. (There are now three(!) assertion introspection methods in py.test: two reinterpretation implementations as well as rewriting) I just hope I haven't scared you completely off py.test! :)

217 comments:

«Oldest   ‹Older   201 – 217 of 217
DRAGON said...

الأعمال الخشبية تحتاج إلى فني لديه خبرة في القياس والتركيب والتشطيب، خصوصا عند إصلاح الأبواب أو تركيب خزائن أو تعديل أثاث داخل المنزل. التنفيذ الجيد يجعل الشكل النهائي أفضل ويطيل عمر القطع الخشبية.

لذلك عند الحاجة إلى صيانة أو تركيب أعمال خشبية، يمكن الاعتماد على نجار عجمان لتنفيذ المطلوب بشكل عملي ومناسب لطبيعة المكان.

Multisoft Virtual Academy said...

This is an excellent walkthrough for developers exploring Grails with modern front-end technologies. The clear explanations and real-world approach make the concepts easy to follow. Even today, the architectural ideas shared here remain valuable for understanding full-stack application development. Thanks for the insightful post!
Oracle Fusion SCM Training
Salesforce Sales Cloud Certification Training
SAP IS Utilities Training
SAP BRIM Training
SAP Document and Reporting Compliance (DRC) Training
Six Sigma Green Belt Training
SailPoint Identity Security Cloud (ISC) Training

Davidsun said...


Organic Desiccated Coconut
Organic Coconut Chips

seo said...

تيورات سهره بألوان مميزة تناسب جميع المناسبات والسهرات الرسمية . اكتشفي تشكيلتنا الراقية من التيورات عالية الجودة لتظهري بإطلالة أنيقة من أماني بوتيك بأفضل الاس...
تيورات نسائية

david said...

arc flash safety gear offers reliable protection for technicians performing routine electrical inspections.

micheal said...

AGP 40 Cal Arc Clothing continues to deliver dependable workplace protection for electricians and maintenance professionals working in challenging industrial environments every day

Felix Solutions said...

nice article, waiting for your another :)
Best medical coding institute in Hyderabad
Best medical coding institute in Chennai
Best medical coding institute in Pune

david said...

flame resistant coveralls provide dependable protective clothing for demanding workplace applications.

stacy gorge said...

All suits are made with DuPont™ Nomex® & DuPont™ Kevlar® for extra protection and comfort .Heavy PPE can slow you down your movement. Arclash® combines lightweight multi layered protection, helping you move easier, work longer and stay protected in demanding arc flash environments. Every Arclash product is developed with a clear objective to help safety professionals, employers and workers confidently manage the risks associated with electrical and thermal hazards while meeting the performance expectations of modern industry .Arclash 60 Cal Arc Flash Protection Suit is made for the strong and reliable protection. No compromise on the safety standards this Arclash suit arc flash ppe clothing provides the extraordinary arc flash protection with the maximum comfort and agility due to the light weight. The suit construction consist on DuPont™ Nomex® & DuPont™ Kevlar® which gives the you the extra edge of protection and comfort.Arclash has the complete range of arc flash suits and garments .Protection starts from HRC 2, HRC 3 & HRC 4

stacy gorge said...

Arclash 60 Cal Arc Flash Protection Suit is made for the strong and reliable protection. No compromise on the safety standards this Arclash suit arc flash ppe clothing provides the extraordinary arc flash protection with the maximum comfort and agility due to the light weight. The suit construction consist on DuPont™ Nomex® & DuPont™ Kevlar® which gives the you the extra edge of protection and comfort. Arclash has the complete range of arc flash suits and garments .Protection starts from HRC 2, HRC 3 & HRC 4 and All suits arc flash ppe clothing

are made with DuPont™ Nomex® & DuPont™ Kevlar® for extra protection and comfort .Heavy PPE can slow you down your movement. Arclash® combines lightweight multi layered protection, helping you move easier, work longer and stay protected in demanding arc flash environments. Every Arclash product is developed with a clear objective to help safety professionals, employers and workers confidently manage the risks associated with electrical and thermal hazards while meeting the performance expectations of modern industry .

stacy gorge said...

Arclash has the complete range of arc flash suits and garments .Protection starts from HRC 2, HRC 3 & HRC 4 and All suits are made with DuPont™ Nomex® & DuPont™ Kevlar® for extra protection and comfort .Heavy PPE can slow you down your movement. Arclash® combines lightweight multi layered protection, helping you move easier, work longer and stay protected in demanding arc flash environments. Every Arclash product is developed with a clear objective to help safety professionals, employers and workers confidently manage the risks associated with electrical and thermal hazards while meeting the performance expectations of modern industry .Arclash 60 Cal Arc Flash Protection Suit is made for the strong and reliable protection. No compromise on the safety standards this Arclash suit arc flash ppe clothing provides the extraordinary arc flash protection with the maximum comfort and agility due to the light weight. The suit construction consist on DuPont™ Nomex® & DuPont™ Kevlar® which gives the you the extra edge of protection and comfort.

stacy gorge said...

Arclash arc flash ppe clothing
has the complete range of arc flash suits and garments .Protection starts from HRC 2, HRC 3 & HRC 4 and All suits are made with DuPont™ Nomex® & DuPont™ Kevlar® for extra protection and comfort .Heavy PPE can slow you down your movement. Arclash® combines lightweight multi layered protection, helping you move easier, work longer and stay protected in demanding arc flash environments. Every Arclash product is developed with a clear objective to help safety professionals, employers and workers confidently manage the risks associated with electrical and thermal hazards while meeting the performance expectations of modern industry .Arclash 60 Cal Arc Flash Protection Suit is made for the strong and reliable protection. No compromise on the safety standards this Arclash suit arc flash ppe clothing provides the extraordinary arc flash protection with the maximum comfort and agility due to the light weight. The suit construction consist on DuPont™ Nomex® & DuPont™ Kevlar® which gives the you the extra edge of protection and comfort.

seo said...

الترا كلين - أفضل شركة تنظيف في الكويت - تنظيف منازل - غسيل كنب - تنظيف بعد التشطيب - خدمة 24/7 - أسعار منافسة | اتصل الآن 96550162626
شركة تنظيف

seo said...

سمو الاناقة لبيع المستلزمات الرجالية نقدم كل ما تحتاجة من اشمغة و غتر و ثياب و مشالح ملكية و ملابس احرام واثواب السفير تشكيلة من الماركات العالمية.
اشمغة

seo said...

يوفر متجر ايكة هوم طاولات طعام وضيافة وخدمة وكراسي ودولاب بوفيه وطاولات تلفزيون وجلسات خارجية ومداخل استقبال بتصاميم عصرية وجودة عالية
متجر أثاث أونلاين في السعودية

seo said...

اكتشف مجموعة عقال رجالي الفاخرة بمختلف التصاميم والخامات. عقَل تقليدي وعصري بجودة عالية يضيف لمسات من الهيبة تسوق الان من العياف فى السعودية مع تقسيط تابي وتمارا
عقال عربي

seo said...

Visit Advanced Hair Studio Dubai for medically supervised hair transplant, hair restoration, and non-surgical solutions. Book your hair check.
Hair Loss Clinic

«Oldest ‹Older   201 – 217 of 217   Newer› Newest»