<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Mirek Długosz personal website</title><link href="https://mirekdlugosz.com/" rel="alternate"/><link href="https://mirekdlugosz.com/blog/feeds/atom.xml" rel="self"/><id>https://mirekdlugosz.com/</id><updated>2025-09-17T18:24:52+02:00</updated><entry><title>Found on web: Commoncog</title><link href="https://mirekdlugosz.com/blog/2025/found-on-web-commoncog/" rel="alternate"/><published>2025-09-17T18:24:52+02:00</published><updated>2025-09-17T18:24:52+02:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2025-09-17:/blog/2025/found-on-web-commoncog/</id><summary type="html">&lt;p&gt;&lt;a href="https://commoncog.com/"&gt;Commoncog&lt;/a&gt; by Cedric Chin is probably the most influential resource I&amp;#8217;ve discovered on the Internet in the last couple of years. It&amp;#8217;s deep. It&amp;#8217;s grounded. It&amp;#8217;s deliberate. It&amp;#8217;s insightful. It&amp;#8217;s judicious. It&amp;#8217;s nuanced. It&amp;#8217;s well written. It&amp;#8217;s practical. If I were …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;a href="https://commoncog.com/"&gt;Commoncog&lt;/a&gt; by Cedric Chin is probably the most influential resource I&amp;#8217;ve discovered on the Internet in the last couple of years. It&amp;#8217;s deep. It&amp;#8217;s grounded. It&amp;#8217;s deliberate. It&amp;#8217;s insightful. It&amp;#8217;s judicious. It&amp;#8217;s nuanced. It&amp;#8217;s well written. It&amp;#8217;s practical. If I were stranded on a desert island and could only receive updates from five websites, Commoncog would be one of them. It&amp;#8217;s that&amp;nbsp;good.&lt;/p&gt;
&lt;p&gt;Consider &lt;a href="https://commoncog.com/a-framework-for-putting-mental-models-to-practice/"&gt;the series on putting mental models to practice&lt;/a&gt;.
There&amp;#8217;s a very compelling argument for limits of scientific knowledge from practical point of view in part two; and right away other types of knowledge are proposed to fill the gap. Some of them are collectively called Naturalistic Decision Making models, elaborated on in part four. Part five is a real treasure - it provides detailed, actionable advice on developing proficiency in expertise-driven decision making. The whole series gives both theoretical framework and practical guidance with a proven track&amp;nbsp;record.&lt;/p&gt;
&lt;p&gt;These themes are then echoed in &lt;a href="https://commoncog.com/the-secret-heart-of-continuous-improvement/"&gt;The Secret at the Heart of Continuous Improvement&lt;/a&gt;, which covers Plan-Do-Study-Act model - something that you might have heard about before. Nonetheless, Cedric manages to really nail down the importance of how the stages support one another, and the entire model as a whole. He then delivers a knockout punch with revolutionary framing - &lt;span class="caps"&gt;PDSA&lt;/span&gt; provides a methodology for single subject studies. It&amp;#8217;s the ultimate tool for finding and refining what works for &lt;strong&gt;you&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;There&amp;#8217;s also entire series on &lt;a href="https://commoncog.com/learning-ill-structured-domains-series/"&gt;Cognitive Flexibility Theory, which tries to uncover how people are learning in ill-structured domains&lt;/a&gt; - and provides tools to accelerate that learning. One of them is case library, which is explained in depth in &lt;a href="https://commoncog.com/how-note-taking-can-help-you-become-an-expert/"&gt;How Note Taking Can Help You Become an Expert&lt;/a&gt;. It acknowledges the messiness of the real world and builds on the insight that experts are pattern-matching novel situations against all the cases they experienced or heard of, identifying similarities and differences. Becoming an expert is largely about growing this body of cases you can&amp;nbsp;reference.&lt;/p&gt;
&lt;p&gt;I&amp;#8217;m truly captivated by these ideas. Where I stand right now, I believe that one of the most beneficial learning resources for testers is building a shared case library. I&amp;#8217;ve been modestly contributing to that in this very blog. If this is something you would be interested in and would like to work on together, &lt;a href="https://mirekdlugosz.com/contact.html"&gt;just send me a message&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Meanwhile, be sure to check out existing &lt;a href="https://commoncog.com/"&gt;Commoncog&lt;/a&gt; article base and subscribe to their newsletter or &lt;span class="caps"&gt;RSS&lt;/span&gt;&amp;nbsp;feed.&lt;/p&gt;</content><category term="Blog"/><category term="found on web"/><category term="planet AST"/><category term="planet MoT"/><category term="planet Python"/><category term="testing"/></entry><entry><title>Experience report: Implementing High Volume Automated Testing system</title><link href="https://mirekdlugosz.com/blog/2025/experience-report-implementing-high-volume-automated-testing-system/" rel="alternate"/><published>2025-08-06T18:23:20+02:00</published><updated>2025-08-06T18:23:20+02:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2025-08-06:/blog/2025/experience-report-implementing-high-volume-automated-testing-system/</id><summary type="html">&lt;p&gt;I first heard about High Volume Automated Testing in 2017, and I&amp;#8217;ve wanted to try it ever since. The opportunity came in 2022, when I was working on revamping a &lt;span class="caps"&gt;UI&lt;/span&gt; testing framework for software called Quipucords. This article focuses on the decision points I encountered, the alternatives I …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I first heard about High Volume Automated Testing in 2017, and I&amp;#8217;ve wanted to try it ever since. The opportunity came in 2022, when I was working on revamping a &lt;span class="caps"&gt;UI&lt;/span&gt; testing framework for software called Quipucords. This article focuses on the decision points I encountered, the alternatives I considered, and the choices I made. At the end, I share the lessons I learned from this&amp;nbsp;experience.&lt;/p&gt;
&lt;p&gt;Keep in mind that this is not intended to apply to all possible High Volume Automated Testing systems, nor is it meant to be an exhaustive list of such system design decisions. I can&amp;#8217;t talk about alternatives I didn&amp;#8217;t think of, I have not actually tried all the alternatives, and I was working in the context of the &lt;span class="caps"&gt;UI&lt;/span&gt; layer of a specific&amp;nbsp;product.&lt;/p&gt;
&lt;p&gt;I share &lt;a href="#sample-implementation"&gt;links to my implementation near the end of the article&lt;/a&gt;. Looking at the code might help to understand some of the more abstract parts for the&amp;nbsp;article.&lt;/p&gt;
&lt;p&gt;But before we begin, let&amp;#8217;s briefly talk about High Volume Automated&amp;nbsp;Testing.&lt;/p&gt;
&lt;h2 id="what-is-high-volume-automated-testing"&gt;&lt;a class="toclink" href="#what-is-high-volume-automated-testing"&gt;What is High Volume Automated&amp;nbsp;Testing&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;High Volume Automated Testing (HiVAT) takes the idea that automation changes the very concept of how tests can be created, run and evaluated, and extends it to its logical conclusion. When learning about test automation, people tend to think about programming a computer to do the same things that a human would do when interacting with a software, but faster and unaffected by boredom or fatigue. HiVAT is born from the question: why stop there? If you program a computer to interact with a software, you can program it to do things that would be too costly for a human to do, or which a human could never&amp;nbsp;do.&lt;/p&gt;
&lt;p&gt;I first heard about it in 2017, from the &lt;a href="https://kaner.com/?p=278"&gt;article published by Cem Kaner in 2013&lt;/a&gt;, but the idea is much older than that. Kaner gave a couple of talks on the topic in 2003 and 2004. He claims to have experienced systems like that first-hand in 1980s, and in various places he mentions prior work dating back to 1970s and even 1960s. However, the phrase &amp;#8220;High Volume Automated Testing&amp;#8221; and the acronym &amp;#8220;HiVAT&amp;#8221; are relatively obscure. They don&amp;#8217;t seem to be discussed in the community often, and I reckon most testers have never heard of&amp;nbsp;them.&lt;/p&gt;
&lt;p&gt;These days, the ideas behind HiVAT see mainstream recognition in property-based testing and&amp;nbsp;fuzzing.&lt;/p&gt;
&lt;p&gt;I highly recommend reading the &lt;a href="https://kaner.com/?p=278"&gt;Kaner article&lt;/a&gt; for a better explanation and full background before moving&amp;nbsp;on.&lt;/p&gt;
&lt;h2 id="designing-a-hivat-system"&gt;&lt;a class="toclink" href="#designing-a-hivat-system"&gt;Designing a HiVAT&amp;nbsp;system&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Kaner lists twelve techniques that he considers to be examples of HiVAT. So the very first decision to make is: do you want to implement one of already known techniques? If so, which one? If not, how is it different from established&amp;nbsp;solutions?&lt;/p&gt;
&lt;p&gt;I wanted a robot that is able to automatically and autonomously wander through the application web interface. It should click random links, fill the forms, edit and delete items where possible etc. The idea was that it should work without supervision for some time, say 15 to 30 minutes. During that time it would most likely cover all the same scenarios we explicitly covered in the standard test suite, but would do so in a random order, while canceling some operations, while repeating certain actions etc. If it encountered any problems, they would likely be caused by the specific data that was introduced or the order of actions that was taken. Ideally, it should just finish the session without any&amp;nbsp;troubles.&lt;/p&gt;
&lt;p&gt;This is one decision that I haven&amp;#8217;t considered too comprehensively. It was kind of obvious for me that in the context I was working in, this is how HiVAT would look&amp;nbsp;like.&lt;/p&gt;
&lt;p&gt;In Python-inspired pseudo-code, the entire system may be approximated as a relatively simple loop. That loop doubles as a map of the main decisions that we have to&amp;nbsp;make.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;browser_driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;create_browser_driver&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;should_still_run&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
   &lt;span class="n"&gt;methods&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_methods_available_at_this_place&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
   &lt;span class="n"&gt;chosen_method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random_choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;method_arguments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_arguments_required_for_method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chosen_method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
       &lt;span class="n"&gt;chosen_method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method_arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
       &lt;span class="c1"&gt;# could be a bug!&lt;/span&gt;
       &lt;span class="n"&gt;handle_exception&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="what-are-the-system-stopping-criteria"&gt;&lt;a class="toclink" href="#what-are-the-system-stopping-criteria"&gt;What are the system stopping&amp;nbsp;criteria?&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;An autonomous testing session can last a long time, but it can&amp;#8217;t run forever. So when should it stop? How can a system decide if it should keep&amp;nbsp;going?&lt;/p&gt;
&lt;p&gt;The obvious solution is to just let it run, basically outsourcing the decision outside of the system. It will stop when the process is killed. That can be done&amp;nbsp;using &lt;code&gt;kill&lt;/code&gt; command, by shutting down the machine, forcing a &lt;span class="caps"&gt;CI&lt;/span&gt; environment to stop execution etc. That approach has two fundamental problems: it&amp;#8217;s not particularly user-friendly and it might bring additional complexity if you need to clean up. Signal might be received in the middle of the loop, and you don&amp;#8217;t control when exactly it is&amp;nbsp;sent.&lt;/p&gt;
&lt;p&gt;Other approach is to let it run until the first exception, signaling a real issue. This builds on the assumption that issue might leave a system in erratic state that invalidates any later actions. The main drawback is that the faulty state might not apply to the entire system, and you lose the opportunity to observe how it operates for a longer period or under larger data volumes.
I imagine that for most systems, invalid data causes them to enter a partially broken but still functional&amp;nbsp;state.&lt;/p&gt;
&lt;p&gt;You can stop the execution after a number of loop iterations. The main problem is that actions are essentially random, and they exercise the system unevenly - a simple navigation between pages has different impact than creating a report that needs to be computed in background task. If you are unlucky, even a relatively large number of iterations might end up executing mostly vain&amp;nbsp;actions.&lt;/p&gt;
&lt;p&gt;You can stop the execution after a certain time. This has similar problem to counting loop iterations, but this time it&amp;#8217;s longer actions that take undue part of the assigned budget. It might happen that a long session did relatively few actions, because all of them happened to take a lot of&amp;nbsp;time.&lt;/p&gt;
&lt;p&gt;Finally, you can try to introduce some system-specific stopping criteria. One way would be keeping separate track of low-impact and high-impact actions, and stopping when both are exceeded. Another would be keeping track of the number of objects&amp;nbsp;created.&lt;/p&gt;
&lt;p&gt;I decided to go with loop iterations counter. That was very easy to implement and at this stage I intended to run the tool exclusively on a local machine. I assumed that after a few sessions I will be in a better position to decide if things should stay that way, or change. I wasn&amp;#8217;t too worried about placing uneven demands on the system during execution, as these should smooth out across many&amp;nbsp;sessions.&lt;/p&gt;
&lt;h3 id="which-actions-should-be-available-to-the-system"&gt;&lt;a class="toclink" href="#which-actions-should-be-available-to-the-system"&gt;Which actions should be available to the&amp;nbsp;system?&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;At every step, HiVAT should do &amp;#8220;a random thing&amp;#8221;. But what does that mean? Are there any actions that should never be selected? How can a system know what actions are&amp;nbsp;available?&lt;/p&gt;
&lt;p&gt;In user interface, a single action may be broken down into multiple steps or interactions. Think about editing an existing object, like a user address. Usually user would have to click &amp;#8220;Edit&amp;#8221; button, which opens a form with address fields. After modifying the form user needs to click &amp;#8220;Save&amp;#8221; for data to be persisted in the system. Clicking &amp;#8220;Cancel&amp;#8221; discards&amp;nbsp;changes.&lt;/p&gt;
&lt;p&gt;There&amp;#8217;s no general consensus how that should be modeled by the test framework. Some people opt to provide methods for modifying a form, and a separate method for submitting changes. But that makes test code more verbose and ties tests to a specific implementation (what if form changes are saved automatically in the new version?). So other people think there should be a single method that both modifies a form and submits changes. But then the framework loses the ability to test a scenario where form is modified and user decides to cancel changes. This trade-off is explained in depth in &lt;a href="https://medium.com/@blakenorrish/going-deeper-into-the-page-object-model-4aee634d9c98"&gt;&amp;#8220;Going Deeper into the Page Object Model&amp;#8221; article by Blake Norrish&lt;/a&gt;. On a side note, highly recommended&amp;nbsp;read.&lt;/p&gt;
&lt;p&gt;The main problem is that the solution you might prefer from the point of view of a generic testing framework might not be the same solution that you would pick for autonomous software traversing&amp;nbsp;program.&lt;/p&gt;
&lt;p&gt;In my case, I decided to do both. Page objects provided separate methods for granular actions, like changing the form and clicking &amp;#8220;Submit&amp;#8221;. At the same time, they also provided more abstract methods representing user intent, like &amp;#8220;modify the address&amp;#8221;. Usually this more abstract method would just call a sequence of methods for more specific actions. While the traditional tests used abstract methods exclusively, the system I was developing was operating only on concrete&amp;nbsp;methods.&lt;/p&gt;
&lt;p&gt;The specifics of how the main loop may obtain available actions depend on the programming language and this is something that might be worth considering before writing a single line of code. You probably want a language with rich runtime introspection capabilities, so you can take a random object and just see what methods it has available. Dynamic dispatch seems to also be a must. Language that offers interpreted runtime with dynamic typing is going to have an edge over the language that requires compilation and static&amp;nbsp;typing.&lt;/p&gt;
&lt;p&gt;The problem was relatively easy to solve in Python with type hints, which was what I used. I&amp;#8217;m not sure if I would be able to solve it in Rust, but that might say more about my Rust expertise than the language&amp;nbsp;itself.&lt;/p&gt;
&lt;h3 id="how-can-a-system-call-a-method-that-requires-arguments"&gt;&lt;a class="toclink" href="#how-can-a-system-call-a-method-that-requires-arguments"&gt;How can a system call a method that requires&amp;nbsp;arguments?&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Conceptually, you can consider this an implementation detail of a previous section. Methods and functions usually require arguments. How can a system know what arguments are required? How can it differentiate valid values from invalid, and handle them&amp;nbsp;appropriately?&lt;/p&gt;
&lt;p&gt;Answer to the first question really depends on a programming language and features it offers. Similar to the previous section, languages with rich introspection capabilities will generally make it much easier than ones without. Here, however, a very strict typing will prove easier to use than a system with dynamic typing. If you need to pass &amp;#8220;something&amp;#8221; to a function for it to work, it helps to know the exact definition of &amp;#8220;something&amp;#8221;. Practically speaking, you might need to create rich library of data transfer object classes that will model arguments taken by various methods across the system. It&amp;#8217;s even better if library can quickly generate DTOs filled with data. In case of Python, using type hints is almost&amp;nbsp;required.&lt;/p&gt;
&lt;p&gt;The second question refers not only to type validation, but also a business validation. If you have a method to delete a user, the argument of that method is not as much a string, but a string that represents the name of a user that exists in the system. Proper autonomous system needs a way of knowing if given input will pass system validation or not, and how to handle the second&amp;nbsp;case.&lt;/p&gt;
&lt;p&gt;For simplicity, I decided to stick to valid data. That changes a problem a little, as now my tool needs a way of obtaining such data, and possibly creating it on demand. Usually that can be done by manipulating the system database directly, or using APIs to manage the data. In my case it was not that much of a problem, as testing framework already had &lt;span class="caps"&gt;API&lt;/span&gt; tests and ability to obtain and create data. I only needed to wire these modules to the code I was&amp;nbsp;writing.&lt;/p&gt;
&lt;p&gt;One obvious alternative is for HiVAT to keep track of data it created (or deleted), and use that whenever existing data is needed to perform a step. That quickly gets complicated if your data has dependencies - testing software might want to perform a step that depends on specific data, but it would not be possible if it didn&amp;#8217;t happen to create that data first. This can be overcome with larger number of iterations in a single&amp;nbsp;session.&lt;/p&gt;
&lt;p&gt;This is one of the decision points that were mostly theoretical for me. I was extending an existing testing framework written in Python, and changing programming language was not up for debate. So I didn&amp;#8217;t need to weigh the options carefully as much as I had to figure out how to solve this specific problem in Python. As I mentioned, I decided to use type hints liberally and used introspection capabilities built into the&amp;nbsp;language.&lt;/p&gt;
&lt;h3 id="how-to-handle-actions-history"&gt;&lt;a class="toclink" href="#how-to-handle-actions-history"&gt;How to handle actions&amp;nbsp;history?&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Should the history of actions be tracked? How? Where should it be stored? Should it be displayed? How should it look like, then? How can you make the history machine-readable, if that&amp;#8217;s&amp;nbsp;desired?&lt;/p&gt;
&lt;p&gt;This is one of the few decision points where you don&amp;#8217;t &lt;em&gt;really&lt;/em&gt; have to choose, but instead you can have your cake and eat it too. You can both save all the actions in more human-readable form in a log file and keep machine-readable format in memory during a session. You can serialize data from memory at the end of the session, or only if there was an issue during execution. Contrary to most of other decision points, there&amp;#8217;s no real reason why one choice here should prevent you from also doing&amp;nbsp;another.&lt;/p&gt;
&lt;p&gt;One of the most consequential choices is deciding where to store a list of actions. You can keep it in system memory, or you can persist it on a disk. The trade-offs are well known. If you decide to keep it in the memory, then you can lose everything if the entire system crashes. You also use the computer memory, making it unavailable to other processes, but that&amp;#8217;s probably negligible unless you run billions of actions. If you save to disk, you might need to think about ways of reading it&amp;nbsp;back.&lt;/p&gt;
&lt;p&gt;If you keep the data in system memory, you should probably model it as some kind of structured information. Depending on the programming language, you might also have an option to keep references to existing classes, instances or functions. This would make it much easier to use them if needed. You can always easily turn that into human-readable form. The alternative - keeping human-readable strings in memory - appears to be all downside and I struggle to take it&amp;nbsp;seriously.&lt;/p&gt;
&lt;p&gt;It&amp;#8217;s important to decide when the data is going to be displayed. I can think of four options: never, in real time (streaming, display action as soon as it is taken), at the end of the session and only in the case of failure. Displaying in real time and displaying in case of failure seem like main options to consider. The risk of displaying in case of failure is that a bug in HiVAT might prevent information from being displayed at all, which is especially problematic in case of critical failure of software under test. If you decide to display information in a streaming fashion, you might end up with a lot of information at the end of session where generally nothing noteworthy&amp;nbsp;happened.&lt;/p&gt;
&lt;p&gt;Finally, some actions might involve sensitive data, like private information or passwords. They probably should be protected, so they should be hidden or filtered out before being displayed. At the same time this is an information loss that would prevent fully reproducible session replays. If this is a concern, you should probably have different ways of storing and accessing human-readable and machine-readable&amp;nbsp;data.&lt;/p&gt;
&lt;p&gt;I decided to keep the history information in memory. Actions were modeled by a special class. A test runner would print action history only in the case of failure. When printed, actions were somewhere between human-readable and machine-readable. I did not go out of my way to make them easy for humans to read, but eventually I did not need to read them programmatically&amp;nbsp;either.&lt;/p&gt;
&lt;p&gt;Since there was no sensitive data that would require additional carefulness in handling, I did not implement any kind of filtering or hiding mechanism. However, this is something I did consider&amp;nbsp;briefly.&lt;/p&gt;
&lt;h3 id="should-the-system-allow-for-a-control-of-certain-sequences"&gt;&lt;a class="toclink" href="#should-the-system-allow-for-a-control-of-certain-sequences"&gt;Should the system allow for a control of certain&amp;nbsp;sequences?&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The HiVAT system I envisioned relied heavily on randomness, but it&amp;#8217;s not hard to think about cases where it could be beneficial to limit it. Should every session be independent from all the previous ones and purely random? Or should there be a way to re-run the previous session? Should it be possible to constrain randomness of certain parts of the session? Which&amp;nbsp;ones?&lt;/p&gt;
&lt;p&gt;Some of these questions might make sense only in the context of an answer to a question posed in the previous section, and some of answers to these questions are self-evident from the answer given in the previous section. If you want the ability to replay the entire session, or any subset of it, then you need a way to store the history of all actions in machine-readable format on a persistent&amp;nbsp;storage.&lt;/p&gt;
&lt;p&gt;The ideal outcome is that if you have a session that finds an issue, you can re-run it on a brand new instance of software with empty database. That would be a significant step towards making all the issues&amp;nbsp;reproducible.&lt;/p&gt;
&lt;p&gt;And imagine you could have a system which takes a session that finds an issue and automatically reduces it to a minimal reproducible path. One session might last 15 minutes, but it doesn&amp;#8217;t mean that everything that happened during that time is equally important. Probably most of things could be safely skipped, as there is relatively short subpath that guarantees the occurrence of the&amp;nbsp;issue.&lt;/p&gt;
&lt;p&gt;These two scenarios were main things I was thinking about when making that decision. It&amp;#8217;s really a binary choice - should you design a system that allows to do these capabilities, or should you just give up on the entire thing? I haven&amp;#8217;t spent too much time considering randomness of specific choices, like always picking up the same item from the list. I feel if I went down this path, there would be a lot of decisions to be made around communicating to the system which parts should be more&amp;nbsp;reproducible.&lt;/p&gt;
&lt;p&gt;While I have not implemented a replay system, and didn&amp;#8217;t spend any time actually thinking how a critical path finder could be built, I decided that my system should be designed in a way that makes such changes in the future possible. In practice it meant identifying possible extension points and leaving around few hooks that might help&amp;nbsp;later.&lt;/p&gt;
&lt;h3 id="how-should-unexpected-errors-be-handled"&gt;&lt;a class="toclink" href="#how-should-unexpected-errors-be-handled"&gt;How should unexpected errors be&amp;nbsp;handled?&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;It&amp;#8217;s almost inevitable that system encounters an issue during a sufficiently long run, either due to a problem in a software under test, or due to a bug in the testing system itself. What should happen&amp;nbsp;then?&lt;/p&gt;
&lt;p&gt;This is something you might have covered when deciding about stopping criteria. One of the options back then was that the system should stop on the first error it can&amp;#8217;t&amp;nbsp;handle.&lt;/p&gt;
&lt;p&gt;But if you decided to stop based on a different condition, now is the time to revisit that choice. Should your system &lt;em&gt;also&lt;/em&gt; stop on an error? Or should it press forward, unreliability of later information be damned? Or should you design more elaborate error handling&amp;nbsp;code?&lt;/p&gt;
&lt;p&gt;First two choices were already discussed in an earlier section, so I&amp;#8217;m going to focus only on the last one. Maybe your testing system can communicate with the software under test and revert it back to a known working state? Or maybe there are some actions that you know how to cancel or revert? Or maybe you can make decisions based on the nature of the issue and context of the action - e.g. transient network errors probably can be ignored or&amp;nbsp;retried.&lt;/p&gt;
&lt;p&gt;In my case, I decided to ignore some errors that I knew can be safely ignored, but make all the others fatal. That decision goes back to an earlier choice to only print actions history in case of error. If I decided to ignore errors and continue running, then I would have to find a way to still print history at the end of the run, or change that other decision and always print the entire history. But then it would make it harder to decide if any given session encountered issues or&amp;nbsp;not.&lt;/p&gt;
&lt;h2 id="sample-implementation"&gt;&lt;a class="toclink" href="#sample-implementation"&gt;Sample&amp;nbsp;implementation&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The HiVAT system I implemented for Quipucords project is &lt;a href="https://github.com/quipucords/camayoc/pull/369/commits/5a937bcc3f6449c4f05d0a47be591bb0d5ff0cae#diff-722cef0e3b0c3aaefa824850b47fa258106568ae03ccdd7987990c13d59ad02cR47"&gt;available at GitHub&lt;/a&gt;. It&amp;#8217;s only 25 lines of&amp;nbsp;code!&lt;/p&gt;
&lt;p&gt;However, that brevity was possible thanks to a number of helpers and design decisions made along the way. The &lt;span class="caps"&gt;UI&lt;/span&gt; framework that made HiVAT possible is introduced in &lt;a href="https://github.com/quipucords/camayoc/pull/365"&gt;Camayoc &lt;span class="caps"&gt;PR&lt;/span&gt; #365&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="lessons-learned"&gt;&lt;a class="toclink" href="#lessons-learned"&gt;Lessons&amp;nbsp;learned&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The main lesson I learned was that my HiVAT was not a good fit for the project. In Quipucords, the most interesting part happens in the backend, when software interacts with the external world. Software that randomly walks the user interface and has limited ability to trigger interactions with the external world is not particularly helpful. I think this kind of tool would be much more applicable for more self-contained systems, like ecommerce, banking or social&amp;nbsp;media.&lt;/p&gt;
&lt;p&gt;Another important lesson is that some programming languages and design approaches can make non-deterministic testing much easier to implement. It definitely helps if you can make choices of technology and framework design while thinking about that particular use-case - it might be hard to bolt onto existing&amp;nbsp;code.&lt;/p&gt;
&lt;p&gt;The kind of HiVAT I created seems to be worth considering if your system meets two&amp;nbsp;requirements:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;It is stateful. This kind of testing requires application to live longer than a single request, ideally longer than any individual session. Most of backend code is like that. Frontend - standing alone - not necessarily. To give you an idea, this would not be a right approach to test a tool like grep, which needs to initialize every time it is&amp;nbsp;started.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Most of the data comes from the user. There are hardly any pure systems like that - everything is interconnected. Online shops are pretty close (the inventory usually needs to be loaded from outside, but after initial setup, it&amp;#8217;s mostly user actions). Banks are also pretty close - obviously there are bank-to-bank transfers, and most banks don&amp;#8217;t allow to do &lt;em&gt;everything&lt;/em&gt; online, but a large part of everyday operations can be modeled this way. Things like Evernote, &lt;span class="caps"&gt;JIRA&lt;/span&gt; or Trello are largely user-generated, but they also offer various integrations with 3rd party&amp;nbsp;services.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Of course if your system does not meet these two requirements, it might still be worth to consider some other kind of&amp;nbsp;HiVAT.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Thanks to &lt;a href="https://qahiccupps.blogspot.com/"&gt;James Thomas&lt;/a&gt; for his feedback and review of an earlier draft of this blog&amp;nbsp;post.&lt;/em&gt;&lt;/p&gt;</content><category term="Blog"/><category term="Python"/><category term="case library"/><category term="planet AST"/><category term="planet MoT"/><category term="planet Python"/><category term="testing"/></entry><entry><title>Interesting bugs: Artifact building failure caused by GitHub API rate limits</title><link href="https://mirekdlugosz.com/blog/2025/interesting-bugs-artifact-building-failure-caused-by-github-api-rate-limits/" rel="alternate"/><published>2025-06-03T18:01:24+02:00</published><updated>2025-06-03T18:01:24+02:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2025-06-03:/blog/2025/interesting-bugs-artifact-building-failure-caused-by-github-api-rate-limits/</id><summary type="html">&lt;p&gt;This is a story of a memorable bug I encountered at work. As with every story of that kind, there are a few ways to look at it: it might save you some time if you encounter a similar issue; it surfaces the hidden work - work that is not visible …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This is a story of a memorable bug I encountered at work. As with every story of that kind, there are a few ways to look at it: it might save you some time if you encounter a similar issue; it surfaces the hidden work - work that is not visible when you only look at the output and artifacts; it demonstrates the benefits of broad knowledge. I&amp;#8217;m sharing it as a contribution to testing &lt;a href="https://commoncog.com/how-note-taking-can-help-you-become-an-expert/"&gt;case library&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I was introducing a Jenkins pipeline to run for every pull request submitted to a project repository. One of the pipeline steps was to build a deployment artifact from the version of code with proposed changes. The artifact was a container&amp;nbsp;image.&lt;/p&gt;
&lt;p&gt;The application was following a standard architecture for web apps, with a separate backend and frontend communicating through an &lt;span class="caps"&gt;HTTP&lt;/span&gt; &lt;span class="caps"&gt;API&lt;/span&gt;. Each component had a separate repository hosted on GitHub and technically had an independent release policy that could result in version numbers that do not match. At the time, whole software was shipped as a single container image comprising both frontend and&amp;nbsp;backend.&lt;/p&gt;
&lt;p&gt;The container image was nominally a backend build artifact. One step in the container building procedure was to download the frontend files. The container build script recognized a special version &amp;#8220;latest&amp;#8221;, which caused script to ask GitHub &lt;span class="caps"&gt;API&lt;/span&gt; for latest releases and extract version of the newest&amp;nbsp;one.&lt;/p&gt;
&lt;p&gt;Not long after the pipeline was introduced, the team noticed that jobs sometimes fail at the container building step. Logs indicated GitHub responded with &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429"&gt;&lt;span class="caps"&gt;HTTP&lt;/span&gt; status 429 Too Many Requests&lt;/a&gt;. This happens when GitHub thinks that the client is exceeding the &lt;span class="caps"&gt;API&lt;/span&gt; rate&amp;nbsp;limits.&lt;/p&gt;
&lt;p&gt;However, the team was small and at the most active days created no more than a few pull requests. Even at the very unlucky timing, the combined pipeline runs from all pull requests should create no more than a dozen or two &lt;span class="caps"&gt;API&lt;/span&gt; requests in an hour. GitHub claimed to allow up to 60 requests per hour and it seemed extremely unlikely that we would routinely exceed&amp;nbsp;that.&lt;/p&gt;
&lt;p&gt;It should be noted that nobody in the team reported any problems with building containers locally. Developers were doing that for months already, often multiple times a&amp;nbsp;day.&lt;/p&gt;
&lt;p&gt;No matter how unlikely it seemed that our pipeline generated more than 60 requests per hour, that&amp;#8217;s &lt;em&gt;some&lt;/em&gt; clue - and one that is cheap to verify. I created a spreadsheet with one column to store the date of pipeline run, and another column to indicate if an error occurred or not. Assuming that rate limiting is the root cause, we would expect failures when there are several runs in the same one-hour window. The earlier ones would succeed, and later ones would fail. Runs that are far removed in time from other runs should never fail. Of course, this pattern was not found in the&amp;nbsp;data.&lt;/p&gt;
&lt;figure&gt;
        &lt;a href="https://mirekdlugosz.com/blog/2025/interesting-bugs-artifact-building-failure-caused-by-github-api-rate-limits/interesting-bugs-artifact-building-failure-caused-by-github-api-rate-limits/errors-tracking-spreadsheet.png"&gt;
            &lt;img src="https://mirekdlugosz.com/blog/2025/interesting-bugs-artifact-building-failure-caused-by-github-api-rate-limits/interesting-bugs-artifact-building-failure-caused-by-github-api-rate-limits/errors-tracking-spreadsheet-min.png" title="Timestamps of pipeline runs and whether GitHub API responded with an error. Rows in green are consistent with the model of exceeding allotted API requests, rows in red are not consistent with the model." alt="Timestamps of pipeline runs and whether GitHub API responded with an error. Rows in green are consistent with the model of exceeding allotted API requests, rows in red are not consistent with the model." loading="lazy"&gt;
        &lt;/a&gt;
        &lt;figcaption&gt;Timestamps of pipeline runs and whether GitHub &lt;span class="caps"&gt;API&lt;/span&gt; responded with an error. Rows in green are consistent with the model of exceeding allotted &lt;span class="caps"&gt;API&lt;/span&gt; requests, rows in red are not consistent with the model.&lt;/figcaption&gt;
    &lt;/figure&gt;
&lt;p&gt;The next step was to learn more about the GitHub &lt;span class="caps"&gt;API&lt;/span&gt; rate limits. When reading a documentation page on that topic, I learned there are multiple pools with different limits. 60 requests per hour is for unauthenticated users, grouped by &lt;span class="caps"&gt;IP&lt;/span&gt; address. Each authenticated user gets 5000 requests per&amp;nbsp;hour.&lt;/p&gt;
&lt;p&gt;At this point I need to share a bit of information about the wider infrastructure. This is subliminal context - something that you learn at some point when working in a company. It is not strictly related to your role and position, so it&amp;#8217;s not something that you would think about constantly; but it&amp;#8217;s a kind of background information you can draw from when thinking about issues that you&amp;nbsp;face.&lt;/p&gt;
&lt;p&gt;Jenkins controller was running inside a container deployed on OpenShift. Most of the work - including building deployment artifacts - was done on short-lived virtual machines provisioned on OpenStack. OpenShift and OpenStack operated from internal company network. The network was set up that outgoing connections to external world were allowed, but they all were routed through a limited number of proxy&amp;nbsp;servers.&lt;/p&gt;
&lt;p&gt;That allowed me to form a hypothesis - our pipeline is rate limited because from GitHub point of view, a sizable chunk of the company appears as a single &lt;span class="caps"&gt;IP&lt;/span&gt; address and is counted against a shared limit. It&amp;#8217;s unlikely for our Jenkins agents to generate over 60 requests during normal operation, but all the development teams sharing OpenStack resources? I can see &lt;em&gt;that&lt;/em&gt;&amp;nbsp;happening.&lt;/p&gt;
&lt;p&gt;This was not a research project, so I wasn&amp;#8217;t exactly interested in refuting or corroborating that hypothesis. It&amp;#8217;s enough that when stated like that, it provides clear steps towards solving the issue. If we authenticate our GitHub &lt;span class="caps"&gt;API&lt;/span&gt; requests, that should convince GitHub to count our requests against a different pool than the one shared with other OpenStack users. It will also increase our limit almost hundredfold, from 60 to&amp;nbsp;5000.&lt;/p&gt;
&lt;p&gt;Requests may be authenticated by including a specific header with a token value. Token management, including ensuring they are not expired, is a separate topic I will not delve into here. In my case, the hardest part was working around the amount of layers that token value needs to be passed through. Jenkins created a virtual machine on OpenStack and provided GitHub &lt;span class="caps"&gt;API&lt;/span&gt; token value in environment variable. Pipeline at one point&amp;nbsp;called &lt;code&gt;make build-container&lt;/code&gt;, which is basically a shortcut for more&amp;nbsp;extensive &lt;code&gt;podman build&lt;/code&gt; command. &lt;code&gt;Containerfile&lt;/code&gt; copied &lt;code&gt;Makefile&lt;/code&gt; from backend project source directory to container build environment, and&amp;nbsp;called &lt;code&gt;make fetch-ui&lt;/code&gt; &lt;strong&gt;there&lt;/strong&gt;. That in turn&amp;nbsp;used &lt;code&gt;curl&lt;/code&gt; to obtain data from the GitHub &lt;span class="caps"&gt;API&lt;/span&gt;. And this is where token needed to be&amp;nbsp;used.&lt;/p&gt;
&lt;figure&gt;
        &lt;a href="https://mirekdlugosz.com/blog/2025/interesting-bugs-artifact-building-failure-caused-by-github-api-rate-limits/interesting-bugs-artifact-building-failure-caused-by-github-api-rate-limits/process-wrappers.png"&gt;
            &lt;img src="https://mirekdlugosz.com/blog/2025/interesting-bugs-artifact-building-failure-caused-by-github-api-rate-limits/interesting-bugs-artifact-building-failure-caused-by-github-api-rate-limits/process-wrappers-min.png" title="Illustration of number of wrappers around a command that actually connects with GitHub API servers. Some wrappers are skipped for brevity, like agent.jar executable." alt="Illustration of number of wrappers around a command that actually connects with GitHub API servers. Some wrappers are skipped for brevity, like agent.jar executable." loading="lazy"&gt;
        &lt;/a&gt;
        &lt;figcaption&gt;Illustration of number of wrappers around a command that actually connects with GitHub &lt;span class="caps"&gt;API&lt;/span&gt; servers. Some wrappers are skipped for brevity, like agent.jar executable.&lt;/figcaption&gt;
    &lt;/figure&gt;
&lt;p&gt;Child processes in Linux by default inherit the environment from their parent, so the basic solution is to ensure a specific environment variable is set by Jenkins and all the layers below indeed recognize it. Two things complicate the picture&amp;nbsp;here.&lt;/p&gt;
&lt;p&gt;First, the container build environment drops most of the surrounding environment variables. This is in part to ensure consistent and reproducible build environment. Variables that you want to use when creating a container need to be explicitly opted&amp;nbsp;in.&lt;/p&gt;
&lt;p&gt;The second thing is worse. Environment variables present in the build stage end up embedded in the final container image. Setting an environment variable fundamentally shares a value with all the people who have access to the container image. When it comes to things like access tokens, this becomes an unacceptable security&amp;nbsp;liability.&lt;/p&gt;
&lt;p&gt;Luckily, the problem was identified in the last few years and the container build tools now support secret mounting. They are special files that are available to containers at the build stage, but are exempted from final container image. It means that at one point after creating a Jenkins agent virtual machine, but before issuing an &lt;span class="caps"&gt;API&lt;/span&gt; request, the environment variable content needs to be written to a file, and that file must be passed as a secret to container build&amp;nbsp;environment.&lt;/p&gt;
&lt;p&gt;The final touch was ensuring that everything still works when an environment variable with a GitHub token is not set. Developers never experienced the problem locally and they shouldn&amp;#8217;t be forced to maintain and set up a GitHub token in their own&amp;nbsp;environment.&lt;/p&gt;
&lt;p&gt;The final version of the solution I implemented at the time can be viewed in &lt;a href="https://github.com/quipucords/quipucords/pull/2483"&gt;GitHub &lt;span class="caps"&gt;PR&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;After merging the &lt;span class="caps"&gt;PR&lt;/span&gt;, we no longer observed pipeline failures at the container image building step caused by GitHub &lt;span class="caps"&gt;API&lt;/span&gt; rate limits. It means that the proposed solution turned out to be&amp;nbsp;correct.&lt;/p&gt;</content><category term="Blog"/><category term="Linux"/><category term="case library"/><category term="planet AST"/><category term="planet MoT"/><category term="planet Python"/><category term="testing"/></entry><entry><title>Understanding Python web deployment</title><link href="https://mirekdlugosz.com/blog/2025/understanding-python-web-deployment/" rel="alternate"/><published>2025-05-22T18:48:09+02:00</published><updated>2025-05-22T18:48:09+02:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2025-05-22:/blog/2025/understanding-python-web-deployment/</id><summary type="html">&lt;p&gt;Trying to deploy server-side Python web application may be overwhelming. Many tutorials will gladly tell you how to run it inside gunicorn, and how to put nginx in front of it, but they usually skim over details on &lt;strong&gt;why&lt;/strong&gt; all this is needed. Why do you need both nginx and …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Trying to deploy server-side Python web application may be overwhelming. Many tutorials will gladly tell you how to run it inside gunicorn, and how to put nginx in front of it, but they usually skim over details on &lt;strong&gt;why&lt;/strong&gt; all this is needed. Why do you need both nginx and gunicorn? Why gunicorn and not Waitress or daphne? What is WhiteNoise and do you still need nginx if you have it? What even &lt;em&gt;is&lt;/em&gt; &lt;span class="caps"&gt;WSGI&lt;/span&gt;?&lt;/p&gt;
&lt;p&gt;In this article I will share my mental model of distinct pieces and how they fit together. If I badly missed a mark somewhere, please &lt;a href="https://mirekdlugosz.com/contact.html"&gt;reach out and tell me how wrong I am&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This is one of those topics that really benefits when you go through it in non-linear fashion. If you are here to learn, you might want to skim over headings and read sections in different order, or re-read selected sections out of order after reading the whole&amp;nbsp;thing.&lt;/p&gt;
&lt;h2 id="the-main-model"&gt;&lt;a class="toclink" href="#the-main-model"&gt;The main&amp;nbsp;model&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Let&amp;#8217;s start with a high-level model of handling &lt;span class="caps"&gt;HTTP&lt;/span&gt;&amp;nbsp;request.&lt;/p&gt;
&lt;figure&gt;
        &lt;a href="https://mirekdlugosz.com/blog/2025/understanding-python-web-deployment/understanding-python-web-deployment/main-model.png"&gt;
            &lt;img src="https://mirekdlugosz.com/blog/2025/understanding-python-web-deployment/understanding-python-web-deployment/main-model-min.png"  alt="Overview of HTTP request/response cycle main model. There are three stages. The first stage is called Accepting, the second stage is called Translate and the third stage is called Process." loading="lazy"&gt;
        &lt;/a&gt;
        
    &lt;/figure&gt;
&lt;p&gt;That model follows the basic &lt;span class="caps"&gt;HTTP&lt;/span&gt; request/response cycle. Server passively waits for connections, which are initiated by clients. Client composes and sends a request. Server generates a response and sends it back, at which point connection is closed. If client decides it needs anything more, it sends new &lt;span class="caps"&gt;HTTP&lt;/span&gt;&amp;nbsp;request.&lt;/p&gt;
&lt;p&gt;The real world is complicated by &lt;span class="caps"&gt;HTTP&lt;/span&gt;/2, websockets and probably other things. &lt;span class="caps"&gt;HTTP&lt;/span&gt;/2 allows server to group multiple responses when addressing a single request. Websockets only start with &lt;span class="caps"&gt;HTTP&lt;/span&gt; request/response before switching to a completely different protocol. Some of these complexities will become relevant&amp;nbsp;later.&lt;/p&gt;
&lt;h2 id="accepting-http-request"&gt;&lt;a class="toclink" href="#accepting-http-request"&gt;Accepting &lt;span class="caps"&gt;HTTP&lt;/span&gt;&amp;nbsp;request&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;span class="caps"&gt;HTTP&lt;/span&gt; may seem simple. Client prepares a text message defining a request and sends it to a server. Server prepares a text message constituting a response and sends it back. This works well for text-based content, such as &lt;span class="caps"&gt;HTML&lt;/span&gt;, &lt;span class="caps"&gt;XML&lt;/span&gt; and &lt;span class="caps"&gt;JSON&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;But there is so much more than that. The current iteration of &lt;span class="caps"&gt;HTTP&lt;/span&gt; standard has a separate document on caching alone, which is about 30 pages long. There are three different compression algorithms, each described in detail in separate documentation. There&amp;#8217;s an optional section about partial access to large resources. Then that whole thing may be wrapped in encrypted frames. &lt;span class="caps"&gt;HTTPS&lt;/span&gt; also involves special procedure for the initial handshake and a fair deal of cryptography. And on top of all that, you need to decide what to do with clients that do not conform to the&amp;nbsp;specification.&lt;/p&gt;
&lt;p&gt;Then, it&amp;#8217;s somewhat rare for a single physical machine to serve single domain (website). On one hand, one machine may serve multiple domains and there must be something that can decide how exactly a specific request directed at one of managed domains should be handled. On the other hand, modern web-based applications are usually distributed across multiple physical machines and there must be something that can choose a specific machine to handle particular incoming request. These days decision is often made based on the request content, analysis of recent traffic and knowledge of internal service&amp;nbsp;infrastructure.&lt;/p&gt;
&lt;p&gt;Unfortunately, the world out there is a dangerous place. Some invalid requests are actually malicious. What if client sends hundreds of thousands of requests? What if client opens a connection and never sends any data? What if client sends less data it claims it will? These sorts of questions do not come naturally from reading the specification alone, and answers have been hard won over the&amp;nbsp;years.&lt;/p&gt;
&lt;p&gt;As you can imagine, there&amp;#8217;s a lot of complexity, depth and lessons learned in last 30 years of &lt;span class="caps"&gt;HTTP&lt;/span&gt; usage. Most application developers don&amp;#8217;t want to deal with any of that - they would rather focus on simple request/response cycle and only dive deep into other areas as they are relevant to problems they face. They would happily outsource everything else to some other&amp;nbsp;software.&lt;/p&gt;
&lt;p&gt;That other software is usually called &lt;span class="caps"&gt;HTTP&lt;/span&gt; server, load balancer or router. Some popular programs in that problem space are &lt;a href="https://nginx.org/"&gt;nginx&lt;/a&gt;, &lt;a href="https://doc.traefik.io/traefik/"&gt;traefik&lt;/a&gt; and &lt;a href="https://kubernetes.io/docs/concepts/services-networking/ingress/"&gt;Kubernetes ingress&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It&amp;#8217;s worth noting that many web app hosting platforms that focus on ease of use have their own setup and do not give any choice in that matter. Part of value proposition of Heroku or Fly.io is that the first stage is taken care of by dedicated professionals, and you can jump straight into step two or even&amp;nbsp;three.&lt;/p&gt;
&lt;h2 id="translating-between-http-and-programming-language"&gt;&lt;a class="toclink" href="#translating-between-http-and-programming-language"&gt;Translating between &lt;span class="caps"&gt;HTTP&lt;/span&gt; and programming&amp;nbsp;language&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Thanks to &lt;span class="caps"&gt;HTTP&lt;/span&gt; server, you have a sequence of bytes that constitute a valid &lt;span class="caps"&gt;HTTP&lt;/span&gt; request directed at your application. Your job is to produce a sequence of bytes that will constitute a valid &lt;span class="caps"&gt;HTTP&lt;/span&gt;&amp;nbsp;response.&lt;/p&gt;
&lt;p&gt;You &lt;em&gt;can&lt;/em&gt; write application that works on these primitives. &lt;a href="https://github.com/python/cpython/blob/main/Lib/http/server.py"&gt;Python&amp;#8217;s built-in &lt;span class="caps"&gt;HTTP&lt;/span&gt; server&lt;/a&gt; is basically that. But for most application developers, this is too low level - they don&amp;#8217;t want to work with sequences of bytes, they want native objects and a library of helpers that make it easy to parse a request and construct a response. Reading and setting a cookie should take one line of code&amp;nbsp;each.&lt;/p&gt;
&lt;p&gt;Translating between a sequences of bytes and &lt;em&gt;some&lt;/em&gt; kind of internal programming language objects (and back from objects to sequence of bytes when processing a response) is the main goal of the second step in the main model. The name and specifics of the approach varies by a programming language. Usually that component is called &amp;#8220;application server&amp;#8221;, &amp;#8220;middleware&amp;#8221; or &amp;#8220;translation&amp;nbsp;framework&amp;#8221;.&lt;/p&gt;
&lt;p&gt;&lt;span class="dquo"&gt;&amp;#8220;&lt;/span&gt;Middleware&amp;#8221; is overloaded term. The concept is also used in traefik, an &lt;span class="caps"&gt;HTTP&lt;/span&gt; server (step number 1), and Django, a Python web application development framework (step number 3). The world is a tangled web and depending on where you draw the lines and how much you are willing to squint, a lot of things are &amp;#8220;in the middle&amp;#8221; between some other&amp;nbsp;things.&lt;/p&gt;
&lt;p&gt;You can think of application server as a tool written in some programming language that helps developers working in that language to work with &lt;span class="caps"&gt;HTTP&lt;/span&gt;, by translating between byte sequences and programming language constructs. There are some exceptions, which I&amp;#8217;ll cover briefly near the end of&amp;nbsp;article.&lt;/p&gt;
&lt;h3 id="python-wsgi"&gt;&lt;a class="toclink" href="#python-wsgi"&gt;Python - &lt;span class="caps"&gt;WSGI&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In Python community, application servers usually are designed to run &lt;span class="caps"&gt;WSGI&lt;/span&gt;-compliant&amp;nbsp;applications.&lt;/p&gt;
&lt;p&gt;&lt;span class="caps"&gt;WSGI&lt;/span&gt; is an abstract specification, published as &lt;a href="https://peps.python.org/pep-0333/"&gt;&lt;span class="caps"&gt;PEP&lt;/span&gt;-333&lt;/a&gt; and &lt;a href="https://peps.python.org/pep-3333/"&gt;&lt;span class="caps"&gt;PEP&lt;/span&gt;-3333&lt;/a&gt;. It&amp;#8217;s like a contract - for application server, it defines what it must provide to application, and what it should expect back. For application, it defines what it must provide back to application server, and what it should expect from it. The idea is that you should be able to run your application on any application server - and application server authors may write code once and support all applications. It&amp;#8217;s an example of adapter design&amp;nbsp;pattern.&lt;/p&gt;
&lt;p&gt;Popular application servers are &lt;a href="https://gunicorn.org/"&gt;gunicorn&lt;/a&gt;, &lt;a href="https://docs.pylonsproject.org/projects/waitress/"&gt;Waitress&lt;/a&gt; and &lt;a href="https://github.com/jonashaag/bjoern"&gt;bjoern&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="ruby-rack"&gt;&lt;a class="toclink" href="#ruby-rack"&gt;Ruby -&amp;nbsp;Rack&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Rack is the main application server for Ruby web applications. These days it is both a concrete implementation of application server software, and &lt;em&gt;de facto&lt;/em&gt; specification of how Ruby applications should work with application servers. &lt;a href="https://github.com/defunkt/unicorn"&gt;unicorn&lt;/a&gt; and &lt;a href="https://puma.io/"&gt;Puma&lt;/a&gt; are independent projects capable of hosting &amp;#8220;Rack&amp;nbsp;applications&amp;#8221;.&lt;/p&gt;
&lt;h3 id="java-jakarta-ee"&gt;&lt;a class="toclink" href="#java-jakarta-ee"&gt;Java - Jakarta &lt;span class="caps"&gt;EE&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Jakarta &lt;span class="caps"&gt;EE&lt;/span&gt; is a bit of overloaded term that may refer to either Jakarta &lt;span class="caps"&gt;EE&lt;/span&gt; software, or Jakarta &lt;span class="caps"&gt;EE&lt;/span&gt; specification. Jakarta &lt;span class="caps"&gt;EE&lt;/span&gt; software is concrete implementation of Jakarta &lt;span class="caps"&gt;EE&lt;/span&gt; specification. As far as I understand, the software was first and later the community formalized whatever it was doing into a&amp;nbsp;specification.&lt;/p&gt;
&lt;p&gt;So Jakarta &lt;span class="caps"&gt;EE&lt;/span&gt; software is a bit similar to Rack, as it&amp;#8217;s a specific software you can use which became a &lt;em&gt;de facto&lt;/em&gt; standard. Jakarta &lt;span class="caps"&gt;EE&lt;/span&gt; specification is a bit similar to &lt;span class="caps"&gt;WSGI&lt;/span&gt;, because it&amp;#8217;s just a document - the idea is that you should be able to use any application server with any application, as long as both are compliant with that&amp;nbsp;document.&lt;/p&gt;
&lt;p&gt;Most notable application servers are &lt;a href="https://tomcat.apache.org/"&gt;Tomcat&lt;/a&gt; and &lt;a href="https://www.wildfly.org/"&gt;WildFly&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="nodejs-built-in"&gt;&lt;a class="toclink" href="#nodejs-built-in"&gt;Node.js -&amp;nbsp;built-in&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Node.js has &lt;span class="caps"&gt;HTTP&lt;/span&gt; server and helpers built in. That&amp;#8217;s because Node.js is built on top of a browser JavaScript engine, and JavaScript in a browser needs to deal with &lt;span class="caps"&gt;HTTP&lt;/span&gt; requests all the time. Node.js, as a &lt;em&gt;de facto&lt;/em&gt; JavaScript backend platform, already does the same thing as application server does for other&amp;nbsp;languages.&lt;/p&gt;
&lt;h3 id="rust-compiled-in"&gt;&lt;a class="toclink" href="#rust-compiled-in"&gt;Rust -&amp;nbsp;compiled-in&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Rust does not provide &lt;span class="caps"&gt;HTTP&lt;/span&gt; helpers in standard library, and there is no generally accepted application server specification. Instead, there are multiple &lt;span class="caps"&gt;HTTP&lt;/span&gt; server libraries that solve many of the problems that your application is likely to deal with across the entire model. When you run application that uses one of these libraries, it binds to a socket and reads and writes byte sequences directly. At runtime, a single Rust application will effectively merge steps number 2 and 3 of the main&amp;nbsp;model.&lt;/p&gt;
&lt;p&gt;Popular libraries are &lt;a href="https://actix.rs/"&gt;Actix Web&lt;/a&gt;, &lt;a href="https://github.com/tokio-rs/axum"&gt;axum&lt;/a&gt; and &lt;a href="https://rocket.rs/"&gt;Rocket&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="adding-value"&gt;&lt;a class="toclink" href="#adding-value"&gt;Adding&amp;nbsp;value&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;At this point &lt;span class="caps"&gt;HTTP&lt;/span&gt; request is in the form suitable for a programming language. This is the step where majority of web application developers spend most of their time. Everything up to this point was a groundwork - things that had to be done, but it makes little difference how exactly they are done. Now we are getting close to the place that differentiates your application from all the other applications in the&amp;nbsp;world.&lt;/p&gt;
&lt;p&gt;In practice, many problems at this point are still shared and there is a room for outsourcing them. The objects and structures exposed to a programming language may still be relatively low level and a bit awkward to work with. Application likely needs to connect to one of popular database engines. A large part of application might be available to authenticated users, so there is a need for authentication framework. There are probably various levels of permissions and roles that users might&amp;nbsp;have.&lt;/p&gt;
&lt;p&gt;These problems are solved by frameworks or libraries. They differ mainly in how many of these problems they solve, and how opinionated they are in solutions they provide. Some popular frameworks are Django (Python), Flask (Python), FastAPI (Python), Quart (Python), Ruby on Rails (Ruby), Grape (Ruby), Spring (Java), Grails (Java), Apache Wicket (Java), Vaadin (Java), Apache Struts (Java), Meteor (JavaScript) or Next.js (JavaScript). In general, communities of most popular languages came up with at least one web application&amp;nbsp;framework.&lt;/p&gt;
&lt;h2 id="noteworthy-exceptions-and-complications"&gt;&lt;a class="toclink" href="#noteworthy-exceptions-and-complications"&gt;Noteworthy exceptions and&amp;nbsp;complications&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id="wsgi-and-asgi"&gt;&lt;a class="toclink" href="#wsgi-and-asgi"&gt;&lt;span class="caps"&gt;WSGI&lt;/span&gt; and &lt;span class="caps"&gt;ASGI&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;span class="caps"&gt;ASGI&lt;/span&gt; is another specification for Python. It serves the same role as &lt;span class="caps"&gt;WSGI&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;Why two specifications? There are two main reasons. First, &lt;span class="caps"&gt;WSGI&lt;/span&gt; is written around &lt;span class="caps"&gt;HTTP&lt;/span&gt; request/response loop, and as such is unable to support newer protocols like websockets. Second, &lt;span class="caps"&gt;WSGI&lt;/span&gt; supports only synchronous functions, while asynchronous code is increasingly popular since it was introduced in Python 3.5. Some application developers concluded that performance gains they receive from asynchronous code is worth more for them than &lt;span class="caps"&gt;WSGI&lt;/span&gt; compatibility. &lt;span class="caps"&gt;ASGI&lt;/span&gt; was created in response to these&amp;nbsp;needs.&lt;/p&gt;
&lt;p&gt;From the main model perspective, &lt;span class="caps"&gt;WSGI&lt;/span&gt; and &lt;span class="caps"&gt;ASGI&lt;/span&gt; are functionally the same&amp;nbsp;thing.&lt;/p&gt;
&lt;p&gt;Practically speaking, gunicorn is go-to &lt;span class="caps"&gt;WSGI&lt;/span&gt; application server and uvicorn is go-to &lt;span class="caps"&gt;ASGI&lt;/span&gt; application server. Django is compatible with both &lt;span class="caps"&gt;WSGI&lt;/span&gt; and &lt;span class="caps"&gt;ASGI&lt;/span&gt;, while Flask users can easily move to Quart, developed by the same team. Some newer frameworks, like FastAPI, are exclusive to &lt;span class="caps"&gt;ASGI&lt;/span&gt;. There is also at least one application server capable of serving either &lt;span class="caps"&gt;WSGI&lt;/span&gt; or &lt;span class="caps"&gt;ASGI&lt;/span&gt;&amp;nbsp;applications.&lt;/p&gt;
&lt;h3 id="whitenoise"&gt;&lt;a class="toclink" href="#whitenoise"&gt;WhiteNoise&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://whitenoise.readthedocs.io"&gt;WhiteNoise&lt;/a&gt; is a Python package for efficient static files serving. Static files are all files not generated dynamically by a Python program, such as stylesheets, frontend scripts, images and&amp;nbsp;fonts.&lt;/p&gt;
&lt;p&gt;It can act as a generic &lt;span class="caps"&gt;WSGI&lt;/span&gt; application wrapping around a target &lt;span class="caps"&gt;WSGI&lt;/span&gt; application. You can think of it as being somewhere between step number 2 and 3 of the main model. However, it also provides facilities to easily integrate with Django, and in such scenario it belongs firmly to step number&amp;nbsp;3.&lt;/p&gt;
&lt;p&gt;The common knowledge of Python community is that static files should not be served by Python application, but instead should be handled by an &lt;span class="caps"&gt;HTTP&lt;/span&gt; server at the earlier stage of request/response cycle. WhiteNoise rejects that notion. It promises a high performance of serving static files while making sure they are correctly cached by &lt;span class="caps"&gt;CDN&lt;/span&gt;, proxies and client. It takes over one of responsibilities of an &lt;span class="caps"&gt;HTTP&lt;/span&gt;&amp;nbsp;server.&lt;/p&gt;
&lt;p&gt;Given all the things that &lt;span class="caps"&gt;HTTP&lt;/span&gt; server does, personally I would not be bold enough to skip it and open WhiteNoise-enabled application server directly to the world. But it might be worth considering in some special cases, like inside a container image or for internal company&amp;nbsp;service.&lt;/p&gt;
&lt;h3 id="phusion-passenger"&gt;&lt;a class="toclink" href="#phusion-passenger"&gt;Phusion&amp;nbsp;Passenger&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://www.phusionpassenger.com/"&gt;Phusion Passenger&lt;/a&gt; is application server notable for two main things. First, it&amp;#8217;s an example of application server written in different language that target application - Phusion Passenger is written in C++ and initially hosted only applications written in Ruby. Second, it&amp;#8217;s an example of application server compatible with multiple programming languages - newer versions can also host Python applications (compatible with &lt;span class="caps"&gt;WSGI&lt;/span&gt;) and Node.js&amp;nbsp;applications.&lt;/p&gt;
&lt;h3 id="granian"&gt;&lt;a class="toclink" href="#granian"&gt;Granian&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://github.com/emmett-framework/granian"&gt;Granian&lt;/a&gt; is application server written in Rust, capable of hosting both &lt;span class="caps"&gt;WSGI&lt;/span&gt; and &lt;span class="caps"&gt;ASGI&lt;/span&gt; Python applications. It&amp;#8217;s notable because it&amp;#8217;s an example of application server written in different language than target application, but also because it&amp;#8217;s application server supporting both Python&amp;nbsp;specifications.&lt;/p&gt;
&lt;h3 id="spring-boot"&gt;&lt;a class="toclink" href="#spring-boot"&gt;Spring&amp;nbsp;Boot&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Spring Boot supports multiple deployment models. It can be built into a package compatible with the Jakarta &lt;span class="caps"&gt;EE&lt;/span&gt; specification, intended to run on application server. But there is also an option to build it into standalone &lt;span class="caps"&gt;HTTP&lt;/span&gt; server application that directly responds to incoming &lt;span class="caps"&gt;HTTP&lt;/span&gt; requests. That option is similar to how &lt;span class="caps"&gt;HTTP&lt;/span&gt; servers are built in&amp;nbsp;Rust.&lt;/p&gt;</content><category term="Blog"/><category term="Linux"/><category term="Python"/><category term="planet AST"/><category term="planet MoT"/><category term="planet Python"/></entry><entry><title>Where Rust fits for me</title><link href="https://mirekdlugosz.com/blog/2025/where-rust-fits-for-me/" rel="alternate"/><published>2025-05-05T17:45:36+02:00</published><updated>2025-05-05T17:45:36+02:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2025-05-05:/blog/2025/where-rust-fits-for-me/</id><summary type="html">&lt;p&gt;I started eyeing Rust around mid-2023. My first program taught me it&amp;#8217;s not a language you can just learn on the job - you have to spend some time on fundamentals or you are going to have a really bad time. I did my part in early 2024 - I have …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I started eyeing Rust around mid-2023. My first program taught me it&amp;#8217;s not a language you can just learn on the job - you have to spend some time on fundamentals or you are going to have a really bad time. I did my part in early 2024 - I have read The Rust Book, went through &lt;a href="https://practice.course.rs/"&gt;Rust By Practice&lt;/a&gt; and &lt;a href="https://rustlings.cool/"&gt;rustlings&lt;/a&gt;. Then I was looking for reasons to keep learning Rust, aside from the fun of it, potential job security and getting to understand programming at the deeper&amp;nbsp;level.&lt;/p&gt;
&lt;p&gt;Current Rust toolchain creates statically-linked binaries that can be run without any special runtime. That binary file is everything that you need to run a program on a specific &lt;span class="caps"&gt;CPU&lt;/span&gt; architecture and specific operating system. It&amp;#8217;s forward compatible and very likely to continue to just work in years to come. I explored that bit in &lt;a href="https://mirekdlugosz.com/blog/2024/rust-binaries-stability/"&gt;an article about binaries stability&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;When you put things this way, Rust is almost the exact opposite of Python. Rust produces single binary, while Python programs tend to consist of multiple files. Rust does not need runtime environment, while Python not only needs Python binary, but also a virtual environment to store all the dependencies. And while Python as a language is stable and takes backwards compatibility seriously, the entire ecosystem is notorious to suffer from bit rot. You can&amp;#8217;t take a random Python program from 2018 and assume everything will just work&amp;nbsp;after &lt;code&gt;pip install -r requirements.txt&lt;/code&gt;. It might, but that&amp;#8217;s very hit and&amp;nbsp;miss.&lt;/p&gt;
&lt;p&gt;That makes Rust a very good choice for things that I expect to run in the future and I want them to just work then. Weeks or months might pass between invocations, but when the time comes, I want as little friction as possible. I want to run the thing, not to deal with uninstallable dependency. At the same time, the problem must be defined well enough that I can expect to want exact same solution in years to&amp;nbsp;come.&lt;/p&gt;
&lt;p&gt;That&amp;#8217;s why I have chosen Rust for my &lt;a href="https://github.com/mirekdlugosz/ebook-name-from-metadata"&gt;ebook file renamer&lt;/a&gt; and &lt;a href="https://github.com/mirekdlugosz/scrapbook/tree/main/gen-from-template-rs"&gt;a tool to generate text from template file&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;These days, when I encounter a task to automate and I think about best programming language to use, I tend to favor three&amp;nbsp;choices.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shell&lt;/strong&gt;. It&amp;#8217;s really only good for setting up environment variables needed by other tools and occasional throwaway loop. I don&amp;#8217;t bother with Bourne Shell compatibility any more, unless I have a strong reason to. I draw the line at functions - if I need functions, I&amp;#8217;ll reach for something&amp;nbsp;else.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Python&lt;/strong&gt;. This is very much my default choice, as this is the language I am most comfortable in. However, backwards compatibility of ecosystem and bootstrapping the environment are major concerns. It&amp;#8217;s a natural choice if I can get away with just a standard library or I can be sure all the required packages are already there in the system, usually pulled in by some system-wide programs installed through standard operating system packages. It&amp;#8217;s also a good choice for one-off and &lt;em&gt;ad hoc&lt;/em&gt; programs that I don&amp;#8217;t expect to run in the future. Finally, it might be a good choice for programs that I work on regularly, like at least once a month - this way ecosystem problems don&amp;#8217;t pile up and can be dealt with one at a&amp;nbsp;time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rust&lt;/strong&gt;. Kind of fallback for cases that don&amp;#8217;t fit well in scenarios described above. Forward-compatible statically linked binary is a good choice for situations where I want a frictionless execution of a program in the future. It works best where problem is small enough to prepare a complete solution in couple of&amp;nbsp;days.&lt;/p&gt;</content><category term="Blog"/><category term="Rust"/><category term="planet AST"/><category term="planet MoT"/><category term="projects"/></entry><entry><title>pytest: running multiple tests with names that might contain spaces</title><link href="https://mirekdlugosz.com/blog/2025/pytest-running-multiple-tests-with-names-that-might-contain-spaces/" rel="alternate"/><published>2025-04-09T20:28:38+02:00</published><updated>2025-04-09T20:28:38+02:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2025-04-09:/blog/2025/pytest-running-multiple-tests-with-names-that-might-contain-spaces/</id><summary type="html">&lt;p&gt;You most certainly know that you can run a single test in entire suite by passing the full&amp;nbsp;path:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;PRODUCT_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;stage&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pytest&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;--critical&lt;span class="w"&gt; &lt;/span&gt;tests/test_mod.py::test_func&lt;span class="o"&gt;[&lt;/span&gt;x1&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This gets old when you want to run around 3 or more tests. In that case, you might end up putting …&lt;/p&gt;</summary><content type="html">&lt;p&gt;You most certainly know that you can run a single test in entire suite by passing the full&amp;nbsp;path:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;PRODUCT_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;stage&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pytest&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;--critical&lt;span class="w"&gt; &lt;/span&gt;tests/test_mod.py::test_func&lt;span class="o"&gt;[&lt;/span&gt;x1&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This gets old when you want to run around 3 or more tests. In that case, you might end up putting paths into a file and passing this file content as command arguments. You probably know that,&amp;nbsp;too:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;PRODUCT_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;stage&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pytest&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;--critical&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&amp;lt;&lt;span class="w"&gt; &lt;/span&gt;/tmp/ci-failures.txt&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;However, this will fail if your test has space in the name (probably as pytest parameter value). Shell still performs command arguments splitting on&amp;nbsp;space.&lt;/p&gt;
&lt;p&gt;To avoid this problem,&amp;nbsp;use &lt;code&gt;cat&lt;/code&gt; and &lt;code&gt;xargs&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;cat&lt;span class="w"&gt; &lt;/span&gt;/tmp/ci-failures.txt&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="nv"&gt;PRODUCT_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;stage&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;xargs&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;200&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;\n&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pytest&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;--critical
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I always thought that xargs runs the command for each line from stdin, so I would avoid it when command takes a long time to start. But turns out, xargs is a little more sophisticated - it can group input lines into subclasses and run a command once for each&amp;nbsp;subclass.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;-n 200&lt;/code&gt; tells xargs to use no more than 200 items in subclass, effectively forcing it to run pytest command&amp;nbsp;once. &lt;code&gt;-d '\n'&lt;/code&gt; tells it to only delimit arguments on newline, removing any special meaning from&amp;nbsp;space.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PRODUCT_ENV&lt;/code&gt; and any other environment variables must be set after the pipe character, or exported beforehand, because each part of shell pipeline is run in a separate&amp;nbsp;subshell.&lt;/p&gt;
&lt;p&gt;After writing this article, I learned that since pytest 8.2 (released in April 2024), you can achieve the same by asking pytest to parse a file for&amp;nbsp;you:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;PRODUCT_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;stage&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pytest&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;--critical&lt;span class="w"&gt; &lt;/span&gt;@/tmp/ci-failures.txt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;However, everything written above still stands for scenarios where any other shell command is used in place&amp;nbsp;of &lt;code&gt;pytest&lt;/code&gt;.&lt;/p&gt;</content><category term="Blog"/><category term="Linux"/><category term="planet AST"/><category term="planet MoT"/><category term="planet Python"/><category term="testing"/><category term="tutorial"/></entry><entry><title>Interesting bugs: peculiar intermittent failure in testing pipeline</title><link href="https://mirekdlugosz.com/blog/2025/interesting-bugs-peculiar-intermittent-failure-in-testing-pipeline/" rel="alternate"/><published>2025-03-26T19:22:30+01:00</published><updated>2025-03-26T19:22:30+01:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2025-03-26:/blog/2025/interesting-bugs-peculiar-intermittent-failure-in-testing-pipeline/</id><summary type="html">&lt;p&gt;Over the years I have encountered my share of memorable problems. They were remarkably complex, hard to debug, completely obvious in retrospect, or plain funny. This is the story of one of&amp;nbsp;them.&lt;/p&gt;
&lt;p&gt;At the beginning, there was a suite of automated tests that I was maintaining. One day one …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Over the years I have encountered my share of memorable problems. They were remarkably complex, hard to debug, completely obvious in retrospect, or plain funny. This is the story of one of&amp;nbsp;them.&lt;/p&gt;
&lt;p&gt;At the beginning, there was a suite of automated tests that I was maintaining. One day one of them failed. Not a big deal, unfortunate reality is that some of them fail sometimes for various reasons. Usually they pass when run again and we can blame unreliable infrastructure, transient networking issue or misalignment of the stars. But few days later the same test failed again. And then again. It was clear that there&amp;#8217;s something going on and this particular test is intermittently failing. I had to figure out what is happening and how can I make the test provide the same result&amp;nbsp;reliably.&lt;/p&gt;
&lt;p&gt;(Note the choice of words here. My goal was not to make the test passing, or &amp;#8220;green&amp;#8221;. There might as well have been a bug in the test itself, or in the product. At this point nobody knew. The main goal was understanding the issue and making sure test is reliably providing the same result - whether it is pass or&amp;nbsp;fail.)&lt;/p&gt;
&lt;p&gt;Before we move on, there&amp;#8217;s some relevant context that I need to share. That suite contained only &lt;span class="caps"&gt;UI&lt;/span&gt; tests. Running them all took about an hour. They were running against staging environment few times a day. The test that was failing was responsible for checking a chart which plots the data from last 30 days. There were other tests verifying other charts, sometimes using different time spans. The website used the same generic chart component in all cases. These other tests never&amp;nbsp;failed.&lt;/p&gt;
&lt;p&gt;On a high level, the failing test consisted of three main steps: request the data from last 30 days using the &lt;span class="caps"&gt;API&lt;/span&gt;, read the data from the graph on the website, and compare both. Test was considered failed if there was any difference between the data from these two sources. Python &lt;a href="https://github.com/seperman/deepdiff"&gt;deepdiff&lt;/a&gt; package was used for comparison. To make it possible, data from &lt;span class="caps"&gt;API&lt;/span&gt; was locally transformed to mimic the structure returned by function responsible for reading the data from &lt;span class="caps"&gt;UI&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;Testing infrastructure had few distinct pieces. There was a Jenkins server that triggered a test suite run at certain times of the day. Job executors were containers in a Kubernetes cluster. To facilitate &lt;span class="caps"&gt;UI&lt;/span&gt; testing, there was a Selenium Grid server with few workers hosted as virtual machines on OpenStack. Tests were running against staging environment of the product, which was also hosted on a Kubernetes cluster, but different than the one where job executors were. I believe all that was scattered across two data centers, with most of testing infrastructure being co-located, and product under test being&amp;nbsp;elsewhere.&lt;/p&gt;
&lt;figure&gt;
        &lt;a href="https://mirekdlugosz.com/blog/2025/interesting-bugs-peculiar-intermittent-failure-in-testing-pipeline/interesting-bugs-peculiar-intermittent-failure-in-testing-pipeline/infrastructure-diagram.png"&gt;
            &lt;img src="https://mirekdlugosz.com/blog/2025/interesting-bugs-peculiar-intermittent-failure-in-testing-pipeline/interesting-bugs-peculiar-intermittent-failure-in-testing-pipeline/infrastructure-diagram-min.png" title="Not necessarily accurate illustration of infrastructure." alt="Not necessarily accurate illustration of infrastructure." loading="lazy"&gt;
        &lt;/a&gt;
        &lt;figcaption&gt;Not necessarily accurate illustration of infrastructure.&lt;/figcaption&gt;
    &lt;/figure&gt;
&lt;p&gt;Now, let&amp;#8217;s get back to the&amp;nbsp;story.&lt;/p&gt;
&lt;p&gt;The very first thing I did was looking into test logs. Unfortunately, differences between objects as reported by deepdiff in this particular case are not easy to read (see below). The amount of data is overwhelming, and displaying everything in single line contributes to the challenge. The log made it clear that lists returned by &lt;span class="caps"&gt;API&lt;/span&gt; and read from &lt;span class="caps"&gt;UI&lt;/span&gt; are different, but it was not immediately obvious where exactly these differences&amp;nbsp;are.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="go"&gt;&amp;gt;       assert not deepdiff.DeepDiff(expected_graph_data, actual_graph_data)&lt;/span&gt;
&lt;span class="go"&gt;E       assert not {&amp;#39;values_changed&amp;#39;: {&amp;quot;root[0][&amp;#39;Date&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: &amp;#39;1970-01-01&amp;#39;, &amp;#39;old_value&amp;#39;: &amp;#39;1970-01-02&amp;#39;}, &amp;quot;root[0][&amp;#39;Foo&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 46, &amp;#39;old_value&amp;#39;: 23}, &amp;quot;root[0][&amp;#39;Bar&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 60, &amp;#39;old_value&amp;#39;: 99}, &amp;quot;root[0][&amp;#39;Total&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 106, &amp;#39;old_value&amp;#39;: 122}, &amp;quot;root[1][&amp;#39;Date&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: &amp;#39;1970-01-02&amp;#39;, &amp;#39;old_value&amp;#39;: &amp;#39;1970-01-03&amp;#39;}, &amp;quot;root[1][&amp;#39;Foo&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 23, &amp;#39;old_value&amp;#39;: 26}, &amp;quot;root[1][&amp;#39;Bar&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 99, &amp;#39;old_value&amp;#39;: 92}, &amp;quot;root[1][&amp;#39;Total&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 122, &amp;#39;old_value&amp;#39;: 118}, &amp;quot;root[2][&amp;#39;Date&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: &amp;#39;1970-01-03&amp;#39;, &amp;#39;old_value&amp;#39;: &amp;#39;1970-01-04&amp;#39;}, &amp;quot;root[2][&amp;#39;Foo&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 26, &amp;#39;old_value&amp;#39;: 49}, &amp;quot;root[2][&amp;#39;Bar&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 92, &amp;#39;old_value&amp;#39;: 86}, &amp;quot;root[2][&amp;#39;Total&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 118, &amp;#39;old_value&amp;#39;: 135}, &amp;quot;root[3][&amp;#39;Date&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: &amp;#39;1970-01-04&amp;#39;, &amp;#39;old_value&amp;#39;: &amp;#39;1970-01-05&amp;#39;}, &amp;quot;root[3][&amp;#39;Foo&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 49, &amp;#39;old_value&amp;#39;: 68}, &amp;quot;root[3][&amp;#39;Bar&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 86, &amp;#39;old_value&amp;#39;: 60}, &amp;quot;root[3][&amp;#39;Total&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 135, &amp;#39;old_value&amp;#39;: 128}, &amp;quot;root[4][&amp;#39;Date&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: &amp;#39;1970-01-05&amp;#39;, &amp;#39;old_value&amp;#39;: &amp;#39;1970-01-06&amp;#39;}, &amp;quot;root[4][&amp;#39;Foo&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 68, &amp;#39;old_value&amp;#39;: 33}, &amp;quot;root[4][&amp;#39;Bar&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 60, &amp;#39;old_value&amp;#39;: 14}, &amp;quot;root[4][&amp;#39;Total&amp;#39;]&amp;quot;: {&amp;#39;new_value...ue&amp;#39;: 25}, &amp;quot;root[24][&amp;#39;Bar&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 29, &amp;#39;old_value&amp;#39;: 78}, &amp;quot;root[24][&amp;#39;Total&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 106, &amp;#39;old_value&amp;#39;: 103}, &amp;quot;root[25][&amp;#39;Date&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: &amp;#39;1970-01-26&amp;#39;, &amp;#39;old_value&amp;#39;: &amp;#39;1970-01-27&amp;#39;}, &amp;quot;root[25][&amp;#39;Foo&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 25, &amp;#39;old_value&amp;#39;: 57}, &amp;quot;root[25][&amp;#39;Bar&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 78, &amp;#39;old_value&amp;#39;: 84}, &amp;quot;root[25][&amp;#39;Total&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 103, &amp;#39;old_value&amp;#39;: 141}, &amp;quot;root[26][&amp;#39;Date&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: &amp;#39;1970-01-27&amp;#39;, &amp;#39;old_value&amp;#39;: &amp;#39;1970-01-28&amp;#39;}, &amp;quot;root[26][&amp;#39;Foo&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 57, &amp;#39;old_value&amp;#39;: 48}, &amp;quot;root[26][&amp;#39;Bar&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 84, &amp;#39;old_value&amp;#39;: 18}, &amp;quot;root[26][&amp;#39;Total&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 141, &amp;#39;old_value&amp;#39;: 66}, &amp;quot;root[27][&amp;#39;Date&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: &amp;#39;1970-01-28&amp;#39;, &amp;#39;old_value&amp;#39;: &amp;#39;1970-01-29&amp;#39;}, &amp;quot;root[27][&amp;#39;Foo&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 48, &amp;#39;old_value&amp;#39;: 89}, &amp;quot;root[27][&amp;#39;Bar&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 18, &amp;#39;old_value&amp;#39;: 14}, &amp;quot;root[27][&amp;#39;Total&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 66, &amp;#39;old_value&amp;#39;: 103}, &amp;quot;root[28][&amp;#39;Date&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: &amp;#39;1970-01-29&amp;#39;, &amp;#39;old_value&amp;#39;: &amp;#39;1970-01-30&amp;#39;}, &amp;quot;root[28][&amp;#39;Foo&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 89, &amp;#39;old_value&amp;#39;: 61}, &amp;quot;root[28][&amp;#39;Bar&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 14, &amp;#39;old_value&amp;#39;: 66}, &amp;quot;root[28][&amp;#39;Total&amp;#39;]&amp;quot;: {&amp;#39;new_value&amp;#39;: 103, &amp;#39;old_value&amp;#39;: 127}}, &amp;#39;iterable_item_added&amp;#39;: {&amp;#39;root[29]&amp;#39;: {&amp;#39;Date&amp;#39;: &amp;#39;1970-01-30&amp;#39;, &amp;#39;Foo&amp;#39;: 61, &amp;#39;Bar&amp;#39;: 66, &amp;#39;Total&amp;#39;: 127}}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Trying to understand this log felt daunting, so my next step was running the failing test locally, in isolation. Predictably, it passed. I didn&amp;#8217;t have the high hopes that I will be able to reproduce the problem right away, but that was a cheap thing to try, so I think it was worth giving a&amp;nbsp;shot.&lt;/p&gt;
&lt;p&gt;At this point I decided there is no way around it and I have to better understand how &lt;span class="caps"&gt;API&lt;/span&gt; and &lt;span class="caps"&gt;UI&lt;/span&gt; responses are different. I copied the log line into editor and inserted a new line character after&amp;nbsp;each &lt;code&gt;},&lt;/code&gt;. Few more changes later I had a form that was a little easier to&amp;nbsp;decipher.&lt;/p&gt;
&lt;p&gt;Deepdiff shows the differences between elements under the same index in lists. But focusing on elements with the same date value revealed that they are fundamentally the same. Values appearing under &amp;#8220;old_value&amp;#8221; in one list appears as &amp;#8220;new_value&amp;#8221; in the other list, just under different index. I have put colored overlay on the screenshot below to make it easier to see. You can think of these lists as mostly the same, but one is shifted when compared to other; or you can say that one list has extra element added at the end, while the other has extra element added at the very beginning. Specifically, &lt;span class="caps"&gt;API&lt;/span&gt; read data from January 2nd to February 1st, but &lt;span class="caps"&gt;UI&lt;/span&gt; displayed data from January 1st to January 31st. There&amp;#8217;s a large overlap, but deepdiff output obscured this key&amp;nbsp;insight.&lt;/p&gt;
&lt;figure&gt;
        &lt;a href="https://mirekdlugosz.com/blog/2025/interesting-bugs-peculiar-intermittent-failure-in-testing-pipeline/interesting-bugs-peculiar-intermittent-failure-in-testing-pipeline/more-readable-log.png"&gt;
            &lt;img src="https://mirekdlugosz.com/blog/2025/interesting-bugs-peculiar-intermittent-failure-in-testing-pipeline/interesting-bugs-peculiar-intermittent-failure-in-testing-pipeline/more-readable-log-min.png" title="Deepdiff output after editing. Color overlays shows that both lists have the same data, but in different places." alt="Deepdiff output after editing. Color overlays shows that both lists have the same data, but in different places." loading="lazy"&gt;
        &lt;/a&gt;
        &lt;figcaption&gt;Deepdiff output after editing. Color overlays shows that both lists have the same data, but in different places.&lt;/figcaption&gt;
    &lt;/figure&gt;
&lt;p&gt;At this point I had an idea what is wrong, but I had no clue why, and why it would affect only this one single test. So in the next step I wanted to see if there are any patterns to the failure. I grabbed test results from last few weeks and put them in the spreadsheet. I added columns for basic things, like the result itself, how long did it take for test to finish, date and time when test was run. To make failing tests visually distinct, I added background color to highlight them. In separate column I tagged all rows where test was running for a first time in a given day. Then I added columns representing known issues that we encountered in previous few weeks, to see if all failures fall into one of&amp;nbsp;them.&lt;/p&gt;
&lt;p&gt;While there wasn&amp;#8217;t a clear and predictable pattern, I did notice a curious thing - &lt;strong&gt;if&lt;/strong&gt; the test failed, it would fail in the first run of a given day. Subsequent runs of any day never failed. And the first run in a day always started shortly after midnight &lt;span class="caps"&gt;UTC&lt;/span&gt;.&lt;/p&gt;
&lt;figure&gt;
        &lt;a href="https://mirekdlugosz.com/blog/2025/interesting-bugs-peculiar-intermittent-failure-in-testing-pipeline/interesting-bugs-peculiar-intermittent-failure-in-testing-pipeline/failures-spreadsheet.png"&gt;
            &lt;img src="https://mirekdlugosz.com/blog/2025/interesting-bugs-peculiar-intermittent-failure-in-testing-pipeline/interesting-bugs-peculiar-intermittent-failure-in-testing-pipeline/failures-spreadsheet-min.png" title="Test results in a spreadsheet" alt="Test results in a spreadsheet" loading="lazy"&gt;
        &lt;/a&gt;
        
    &lt;/figure&gt;
&lt;p&gt;That allowed me to construct a working hypothesis: the issue is somehow related to time and there&amp;#8217;s only a short window when it may occur, maybe up to few hours. That window is located around midnight &lt;span class="caps"&gt;UTC&lt;/span&gt;. Such hypothesis explains why subsequent pipeline runs never failed, and why I was never successful at reproducing the issue locally - I am located east of &lt;span class="caps"&gt;UTC&lt;/span&gt; line and I would have to try running the test way outside of working hours. Of course I didn&amp;#8217;t know if I was up to something or I was just creating complex &lt;em&gt;ad hoc&lt;/em&gt; hypothesis that fits the data. But it directed my next&amp;nbsp;step.&lt;/p&gt;
&lt;p&gt;To corroborate the hypothesis I needed some new information, things I didn&amp;#8217;t have before. To gather it, I have added further logging in the test suite. First, I have used Selenium JavaScript execution capability to obtain the date and time as the browser &amp;#8220;sees&amp;#8221; it. Then I have done the same from Python, which both drives Selenium and requests data from &lt;span class="caps"&gt;API&lt;/span&gt;. The important part is that Python code is executed directly on test runner (container in Kubernetes) and JavaScript code is executed in the browser (Selenium Grid &lt;span class="caps"&gt;VM&lt;/span&gt; on&amp;nbsp;OpenStack). &lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git package/tests/ui/test_failing.py package/tests/ui/test_failing.py&lt;/span&gt;
&lt;span class="gh"&gt;index 1234567..abcdef0 100644&lt;/span&gt;
&lt;span class="gd"&gt;--- package/tests/ui/test_failing.py&lt;/span&gt;
&lt;span class="gi"&gt;+++ package/tests/ui/test_failing.py&lt;/span&gt;
&lt;span class="gu"&gt;@@ -10,6 +10,13 @@ def test_failing_test(user_app, some_fixture):&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;    &amp;quot;&amp;quot;&amp;quot;
&lt;span class="w"&gt; &lt;/span&gt;    view = navigate_to(user_app.some_app, &amp;quot;SomeViewName&amp;quot;)
&lt;span class="gi"&gt;+    browser_time_string = view.browser.execute_script(&amp;quot;return new Date().toTimeString()&amp;quot;)&lt;/span&gt;
&lt;span class="gi"&gt;+    browser_utc_string = view.browser.execute_script(&amp;quot;return new Date().toUTCString()&amp;quot;)&lt;/span&gt;
&lt;span class="gi"&gt;+    view.logger.info(&lt;/span&gt;
&lt;span class="gi"&gt;+        &amp;quot;[JavaScript] Time right now: %s ; UTC time: %s&amp;quot;,&lt;/span&gt;
&lt;span class="gi"&gt;+        browser_time_string,&lt;/span&gt;
&lt;span class="gi"&gt;+        browser_utc_string,&lt;/span&gt;
&lt;span class="gi"&gt;+    )&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;    expected_x_axis = get_xaxis_values()
&lt;span class="w"&gt; &lt;/span&gt;    view.items.item_select(some_value)
&lt;span class="w"&gt; &lt;/span&gt;    view.graph.wait_displayed()
&lt;span class="gh"&gt;diff --git package/utils/utils.py package/utils/utils.py&lt;/span&gt;
&lt;span class="gh"&gt;index 1234567..abcdef0 100644&lt;/span&gt;
&lt;span class="gd"&gt;--- package/utils/utils.py&lt;/span&gt;
&lt;span class="gi"&gt;+++ package/utils/utils.py&lt;/span&gt;
&lt;span class="gu"&gt;@@ -10,6 +10,14 @@ METRIC_MAP = {&lt;/span&gt;


&lt;span class="w"&gt; &lt;/span&gt;def _get_dates_range(some_param=&amp;quot;Daily&amp;quot;, date=None):
&lt;span class="gi"&gt;+    current_time = arrow.now()&lt;/span&gt;
&lt;span class="gi"&gt;+    log.info(&lt;/span&gt;
&lt;span class="gi"&gt;+        &amp;quot;[Python] Time right now: %s ; TZ name: %s ; TZ offset: %s ; UTC time: %s&amp;quot;,&lt;/span&gt;
&lt;span class="gi"&gt;+        current_time,&lt;/span&gt;
&lt;span class="gi"&gt;+        current_time.strftime(&amp;quot;%Z&amp;quot;),&lt;/span&gt;
&lt;span class="gi"&gt;+        current_time.strftime(&amp;quot;%z&amp;quot;),&lt;/span&gt;
&lt;span class="gi"&gt;+        arrow.utcnow(),&lt;/span&gt;
&lt;span class="gi"&gt;+    )&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;    try:
&lt;span class="w"&gt; &lt;/span&gt;        date = arrow.get(date)
&lt;span class="w"&gt; &lt;/span&gt;    except TypeError:
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With the above patch applied and deployed, all I needed to do was waiting for the next failure. I hoped that new logs would reveal some more information once it fails&amp;nbsp;again.&lt;/p&gt;
&lt;p&gt;That turned out to be true. JavaScript showed a date one day earlier than Python. In fact, the time in JavaScript was about 15 minutes earlier than in Python. So if test suite ran around midnight, and we got to offending test within 15 minutes of suite start, then Python would request data through &lt;span class="caps"&gt;API&lt;/span&gt; for some dates, but website in browser would think it is still the previous day, and request different set of dates. It means that the window where issue occurs is extremely small - just around 15 minutes each&amp;nbsp;day.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="go"&gt;[JavaScript] Time right now: Thu Jan 01 1970 23:58:17 GMT+0000 (Coordinated Universal Time) ; UTC time: Thu, 01 Jan 1970 23:58:17GMT&lt;/span&gt;
&lt;span class="go"&gt;[Python] Time right now: 1970-01-02T00:14:36.042473+00:00 ; TZ name: UTC ; TZ offset: +0000 ; UTC time: 1970-01-02T00:14:36.042473+00:00&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This concludes the main part of the debugging story - at this point we knew what is wrong, we knew that failure is not caused by a bug in a test or a product, and it was clear that the solution is for all machines involved in testing to reconcile date and time. It also seemed like the JavaScript shows wrong date, which might mean that the issue is with Selenium Grid machines or OpenStack&amp;nbsp;instance.&lt;/p&gt;
&lt;p&gt;I connected to all Selenium Grid machines using &lt;span class="caps"&gt;SSH&lt;/span&gt; and checked their local time&amp;nbsp;using &lt;code&gt;date&lt;/code&gt; command. They were about 15 minutes behind their wall-clock time. I assumed the difference is caused by various OpenStack and underlying infrastructure maintenance work, so I just&amp;nbsp;used &lt;code&gt;hwclock&lt;/code&gt; to force &lt;span class="caps"&gt;OS&lt;/span&gt; clock to synchronize with hardware clock and moved on with my&amp;nbsp;day.&lt;/p&gt;
&lt;p&gt;Couple of days later I connected to these machines again and noticed that the local time is behind again, but only by about 15 seconds. It looked like the local clock is drifting by about 5 seconds a day. It might not sound like much, but it also meant that it&amp;#8217;s only a matter of time before original issue happens again. Clearly someone logging in to these machines every once in a while and resetting clock would not be a good long term solution - we needed something that can automatically keep time&amp;nbsp;synchronized.&lt;/p&gt;
&lt;p&gt;That something is called &lt;span class="caps"&gt;NTP&lt;/span&gt; and all the machines already had &lt;a href="https://chrony-project.org/"&gt;chrony&lt;/a&gt; installed. However, it didn&amp;#8217;t seem to work correctly. While the commands succeeded and logs did not show any problems, the clock would just not change. After few frustrating hours I think I ruled out all possible causes at the operating system level and came to the conclusion that perhaps the &lt;span class="caps"&gt;NTP&lt;/span&gt; traffic to public servers is blocked by data center firewall. I reached out to OpenStack administrators for help and they told me that there is a blessed &lt;span class="caps"&gt;NTP&lt;/span&gt; server instance inside the data center that I should use. Once I configured chrony to use it as a source, everything finally&amp;nbsp;worked.&lt;/p&gt;
&lt;p&gt;This way browsers started to consistently report the same time as Python executors. That fixed the original issue and we did not observe any test failures caused by it&amp;nbsp;again.&lt;/p&gt;
&lt;!--
feedback questions:
- is this understandable? do you understand what the issue was and how it was solved?
- what feelings does it evoke? do you think "oh yeah, that's pretty cool thing, it would take me hours to do something like that" or "nah, this is obvious and mundane"?
--&gt;</content><category term="Blog"/><category term="Linux"/><category term="Python"/><category term="case library"/><category term="planet AST"/><category term="planet MoT"/><category term="planet Python"/><category term="testing"/></entry><entry><title>Django: data modified in pre_save signal is not saved</title><link href="https://mirekdlugosz.com/blog/2025/django-data-modified-in-pre-save-signal-is-not-saved/" rel="alternate"/><published>2025-03-12T19:09:14+01:00</published><updated>2025-03-12T19:09:14+01:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2025-03-12:/blog/2025/django-data-modified-in-pre-save-signal-is-not-saved/</id><summary type="html">&lt;p&gt;When I was working on my first Django project, I encountered a problem where modifications done inside &lt;a href="https://docs.djangoproject.com/en/dev/ref/signals/#django.db.models.signals.pre_save"&gt;&lt;code&gt;pre_save&lt;/code&gt; signal&lt;/a&gt; were not stored in the database. Back then it took me a couple of hours to figure out what is wrong and how to fix it. I&amp;#8217;m sharing what I …&lt;/p&gt;</summary><content type="html">&lt;p&gt;When I was working on my first Django project, I encountered a problem where modifications done inside &lt;a href="https://docs.djangoproject.com/en/dev/ref/signals/#django.db.models.signals.pre_save"&gt;&lt;code&gt;pre_save&lt;/code&gt; signal&lt;/a&gt; were not stored in the database. Back then it took me a couple of hours to figure out what is wrong and how to fix it. I&amp;#8217;m sharing what I&amp;#8217;ve learned to save that time someone&amp;nbsp;else.&lt;/p&gt;
&lt;p&gt;Imagine the application to track your gym workouts. At the core of application is exercise session, which knows how many repetitions you want to do and how many you have done so far. Once you do enough repetitions, session is considered complete. It could be implemented like&amp;nbsp;that:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;ExerciseSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;target_repetitions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IntegerField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;null&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;blank&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;repetitions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IntegerField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;null&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;blank&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BooleanField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;null&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;blank&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Since &amp;#8220;session is complete once you do enough repetitions&amp;#8221; is a hard rule, it needs to be&amp;nbsp;enforced. &lt;code&gt;pre_save&lt;/code&gt; signal sounds like reasonable place to implement it, as it allows us to have many functions that&amp;nbsp;modify &lt;code&gt;repetitions&lt;/code&gt;. Signal handler might look like&amp;nbsp;that:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@receiver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pre_save&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExerciseSession&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;exercise_session_pre_save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;using&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;update_fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repetitions&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target_repetitions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With the above setup we can modify the model instance and rest assured that exercise session is marked as completed when&amp;nbsp;needed:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;exercise_session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repetitions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="n"&gt;exercise_session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update_fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;repetitions&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Except that&amp;#8230; this doesn&amp;#8217;t work. Once you retrieve this exercise session from&amp;nbsp;database, &lt;code&gt;completed&lt;/code&gt; will be set to&amp;nbsp;False.&lt;/p&gt;
&lt;p&gt;The reason for this is at intersection&amp;nbsp;of &lt;code&gt;pre_save&lt;/code&gt; signal&amp;nbsp;and &lt;code&gt;update_fields&lt;/code&gt; argument. &lt;code&gt;pre_save&lt;/code&gt; is called early&amp;nbsp;in &lt;code&gt;save()&lt;/code&gt; call and may be used to modify the instance before data is saved in database. However, &lt;span class="caps"&gt;SQL&lt;/span&gt; query constructed later&amp;nbsp;in &lt;code&gt;save()&lt;/code&gt; call will contain only the fields listed&amp;nbsp;in &lt;code&gt;update_fields&lt;/code&gt; argument.&lt;/p&gt;
&lt;p&gt;So when calling&amp;nbsp;a &lt;code&gt;save()&lt;/code&gt;, you need to predict&amp;nbsp;that &lt;code&gt;pre_save&lt;/code&gt; signal might modify the instance, and include possibly modified fields&amp;nbsp;in &lt;code&gt;update_fields&lt;/code&gt; list:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;exercise_session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update_fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;repetitions&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;completed&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The complete code used in this blog post is available in &lt;a href="https://github.com/mirekdlugosz/django-pre-save-issue-demo"&gt;GitHub repository&lt;/a&gt;. See&amp;nbsp;the &lt;code&gt;README&lt;/code&gt; file for instructions how to set up and run the&amp;nbsp;project.&lt;/p&gt;</content><category term="Blog"/><category term="Python"/><category term="planet AST"/><category term="planet MoT"/><category term="planet Python"/><category term="tutorial"/></entry><entry><title>Customizing fonts’ look with OpenType features</title><link href="https://mirekdlugosz.com/blog/2025/customizing-fonts-look-with-opentype-features/" rel="alternate"/><published>2025-03-02T19:36:10+01:00</published><updated>2025-03-02T19:36:10+01:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2025-03-02:/blog/2025/customizing-fonts-look-with-opentype-features/</id><summary type="html">&lt;p&gt;Many programming fonts allow customization through OpenType features. For people who are picky about certain properties, like easily&amp;nbsp;distinguishable &lt;code&gt;l&lt;/code&gt; and &lt;code&gt;I&lt;/code&gt; (lowercase L and uppercase i), a single toggle can make a difference between rejecting a font and starting to use it. Unfortunately, choosing the perfect font with the …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Many programming fonts allow customization through OpenType features. For people who are picky about certain properties, like easily&amp;nbsp;distinguishable &lt;code&gt;l&lt;/code&gt; and &lt;code&gt;I&lt;/code&gt; (lowercase L and uppercase i), a single toggle can make a difference between rejecting a font and starting to use it. Unfortunately, choosing the perfect font with the perfect setup is way harder than it should be. This post describes the tools that may help, including the one that I wrote&amp;nbsp;recently.&lt;/p&gt;
&lt;p&gt;Whenever I stumble upon a new font, I want to&amp;nbsp;know:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;how does the font generally&amp;nbsp;look?&lt;/li&gt;
&lt;li&gt;does the font allow customization of specific glyphs that bother&amp;nbsp;me?&lt;/li&gt;
&lt;li&gt;when set up properly, how does the font fare against my current&amp;nbsp;setup?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But first, how do you even learn about a new font? Two proven resources are &lt;a href="https://www.programmingfonts.org/"&gt;programmingfonts.org&lt;/a&gt; and &lt;a href="https://www.codingfont.com/"&gt;codingfont.com&lt;/a&gt;. That topic is also frequently brought up on social media like Reddit in communities discussing editors, terminals and programming in&amp;nbsp;general.&lt;/p&gt;
&lt;p&gt;Let&amp;#8217;s say that &lt;a href="https://www.jetbrains.com/lp/mono/"&gt;JetBrains Mono&lt;/a&gt; caught my attention. The demo page is nice enough, but does not give an option to type in custom text. I am not a fan of ligatures for code (the ones they say &amp;#8220;reduces noise&amp;#8221;), while I appreciate ligatures that &amp;#8220;balance whitespace&amp;#8221; - I wonder if I can disable one category? Also, I prefer slashed&amp;nbsp;zero.&lt;/p&gt;
&lt;p&gt;A few years ago, that would have been the end of it for me. The font looks nice, but there are some details I do not like. Today, I know that many of these details can often be customized through OpenType font features. But how can you learn what features the font supports, and what exactly do they&amp;nbsp;change?&lt;/p&gt;
&lt;p&gt;The best tools that I have found so far are web based: &lt;a href="https://wakamaifondue.com/"&gt;Wakamai Fondue&lt;/a&gt; and &lt;a href="https://www.adamjagosz.com/bulletproof/"&gt;Bulletproof Font Tester&lt;/a&gt;. Other similar tools are &lt;a href="https://www.axis-praxis.org/specimens/"&gt;Axis-Praxis&lt;/a&gt; and &lt;a href="https://fontdrop.info/"&gt;FontDrop!&lt;/a&gt;. All of them allow you to select a font file from local computer to explore its capabilities. Usually you want to select a file with &amp;#8220;medium&amp;#8221; or &amp;#8220;regular&amp;#8221; in&amp;nbsp;name.&lt;/p&gt;
&lt;p&gt;There are a few things that I like about Wakamai Fondue. It tries to display all characters affected by a given setting, so you can learn that in JetBrains Mono, &amp;#8220;Character Variants 04&amp;#8221; affects how letter &amp;#8220;j&amp;#8221; is displayed. You can toggle a setting on and off, making it easier to notice changes. It also gives you the internal name of OpenType font feature, so you know that slashed zero is accessed through a feature&amp;nbsp;called &lt;code&gt;zero&lt;/code&gt;. This becomes relevant later, when you try to apply settings on your&amp;nbsp;machine.&lt;/p&gt;
&lt;p&gt;There are also a few things that I like about Bulletproof Font Tester. It has a large collection of texts, including cases specifically designed to show each letter or common issues. It&amp;#8217;s easier to see how specific set of font features apply to the entire text. It remembers all the fonts you have selected during the session, making comparisons easier. It also provides interface to modify text layout properties, although I never use&amp;nbsp;these.&lt;/p&gt;
&lt;p&gt;The main thing I don&amp;#8217;t like is that there&amp;#8217;s no way to save your settings. It&amp;#8217;s not possible to pause your exploration and resume it another day. If you close a tab, you need to start from scratch. Comparing a new font to the one you are currently using involves setting it up in the tool - possibly multiple times. I have not found a good way to compare multiple fonts to each other. It&amp;#8217;s also not easy to compare effect of two different sets of features on a single&amp;nbsp;font.&lt;/p&gt;
&lt;p&gt;I decided to use recent Day of Learning at Red Hat to see if I can do anything about it. The result is called Font Feature Tester and you can &lt;a href="https://github.com/mirekdlugosz/font-feature-tester"&gt;find it on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;At the very core is a simple command line tool written in Rust that reads a font configuration file and generates an image of provided text set with that&amp;nbsp;font.&lt;/p&gt;
&lt;p&gt;There&amp;#8217;s also a Python script that automates the execution of the Rust tool across multiple configuration files and collects all the output images in a single directory. It&amp;#8217;s responsible for ensuring that images are consistent and use the same text, image size and&amp;nbsp;colors.&lt;/p&gt;
&lt;p&gt;Using web tools mentioned earlier, I can find a set of font features that look promising. Then I prepare a configuration file for each font I want to try - at the very least, that would be my current font and font I consider switching to. Then I run a Python script to generate&amp;nbsp;images.&lt;/p&gt;
&lt;p&gt;I can use a standard image viewer to see how the text looks. When I want to compare multiple fonts, I can open a few images at once and put them side by&amp;nbsp;side.&lt;/p&gt;
&lt;figure&gt;
        &lt;a href="https://mirekdlugosz.com/blog/2025/customizing-fonts-look-with-opentype-features/customizing-fonts-look-with-opentype-features/font-feature-tester-example.png"&gt;
            &lt;img src="https://mirekdlugosz.com/blog/2025/customizing-fonts-look-with-opentype-features/customizing-fonts-look-with-opentype-features/font-feature-tester-example-min.png" title="Images generated by my tool. I would keep one as a reference, and cycle through images in the other" alt="Two images with sample text set in different fonts, displayed side by side" loading="lazy"&gt;
        &lt;/a&gt;
        &lt;figcaption&gt;Images generated by my tool. I would keep one as a reference, and cycle through images in the other&lt;/figcaption&gt;
    &lt;/figure&gt;
&lt;p&gt;That approach scales to any number of fonts and feature sets - you only need to add a new configuration file. Since all the data is written in files and somewhat automated, you can stop at any time and resume&amp;nbsp;later.&lt;/p&gt;
&lt;p&gt;One major gap is that you can&amp;#8217;t easily move your setup between all the tools. Once you find features you like, it&amp;#8217;s up to you to apply the same configuration in your terminal emulator, editor and desktop environment. At the end of the day, the ergonomics of my simple tool depend a lot on ergonomics of your desktop environment and image&amp;nbsp;viewer.&lt;/p&gt;
&lt;h2 id="vision-for-the-future"&gt;&lt;a class="toclink" href="#vision-for-the-future"&gt;Vision for the&amp;nbsp;future&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Font Feature Tester is a barely minimal &lt;em&gt;viable&lt;/em&gt; product. It solves the very specific part of the larger problem, and manages to do that with support from other tools. The integration leaves a lot to desire. I can envision it growing into much more complete solution, so I thought it might be fun to finish this post by role playing a product manager. What is the end state of things? How can we get&amp;nbsp;there?&lt;/p&gt;
&lt;p&gt;I imagine the final version looking something like&amp;nbsp;that:&lt;/p&gt;
&lt;figure&gt;
        &lt;a href="https://mirekdlugosz.com/blog/2025/customizing-fonts-look-with-opentype-features/customizing-fonts-look-with-opentype-features/font-feature-tester-ui.png"&gt;
            &lt;img src="https://mirekdlugosz.com/blog/2025/customizing-fonts-look-with-opentype-features/customizing-fonts-look-with-opentype-features/font-feature-tester-ui-min.png"  alt="Sketch of software window. There are tabs at the top. Main section displays a text set in specific font, using specific OpenType features. There's a section on the left with tools used to change font, OpenType features, colors etc." loading="lazy"&gt;
        &lt;/a&gt;
        
    &lt;/figure&gt;
&lt;p&gt;Each tab would display text set in specific font with specific font features enabled. It should be easy to move between tabs using keyboard shortcuts. There should be an option to disable a tab without closing it - so you can temporarily remove a specific setup from consideration, but you can also bring it back if you change your mind. Alternatively, there could be a history of closed&amp;nbsp;tabs.&lt;/p&gt;
&lt;p&gt;Options on the left side would allow to modify the current text setup. Obviously there needs to be a way to change font and font features, text properties and text content. One of the challenges is that some of these settings sometimes apply to all tabs, while other times should apply only to current tab. It&amp;#8217;s valid to want to increase font size for all tabs, but it&amp;#8217;s also valid to change font size for current tab&amp;nbsp;only.&lt;/p&gt;
&lt;p&gt;It&amp;#8217;s not clear to me how to handle a case where you want to compare images side by side. Some kind of split view is needed, possibly with tab&amp;nbsp;locking.&lt;/p&gt;
&lt;p&gt;The first step on the road towards that goal is introducing the graphical interface. For me, it makes sense to focus on single tab - just a view of image and options in the sidebar. Options can be added&amp;nbsp;iteratively.&lt;/p&gt;
&lt;p&gt;Some options may be improved over time. Font selection may start as a simple text field where the path to font is pasted. Later it may be changed to use file selection widget. Finally it can be integrated with something like fontconfig to also allow selecting fonts from these that are already installed. The same is true for OpenType font features - initially, it may be a text box, but later it should display a list of all font features available in the font, and provide a way to set a value for each feature. While most fonts support simple toggles, there are some fonts that support one of a few values for selected features. Interface should acknowledge&amp;nbsp;that.&lt;/p&gt;
&lt;p&gt;All the changes should be saved in configuration file, so program may be closed at any time. Ideally, current configuration file format would be reused, so the existing workflow could be supported while user interface is being&amp;nbsp;developed.&lt;/p&gt;
&lt;p&gt;Once the single font view is finished, it may be wrapped in a tabbed interface. At that point, the old Python tool-based workflow may be&amp;nbsp;removed.&lt;/p&gt;
&lt;p&gt;At one point, it would be great to ensure that the application works on Windows and&amp;nbsp;macOS.&lt;/p&gt;
&lt;p&gt;I don&amp;#8217;t know how far on that journey I will go, especially since creating desktop applications is not something I find particularly appealing. But if this is something you find interesting, feel free to &lt;a href="https://mirekdlugosz.com/contact.html"&gt;reach out to me&lt;/a&gt;!&lt;/p&gt;</content><category term="Blog"/><category term="Linux"/><category term="Rust"/><category term="planet AST"/><category term="planet MoT"/><category term="planet Python"/><category term="projects"/><category term="tutorial"/></entry><entry><title>Testing a new feature - should you start with automation or by inspecting it yourself?</title><link href="https://mirekdlugosz.com/blog/2025/testing-a-new-feature-should-you-start-with-automation-or-by-inspecting-it-yourself/" rel="alternate"/><published>2025-02-11T19:56:12+01:00</published><updated>2025-02-11T19:56:12+01:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2025-02-11:/blog/2025/testing-a-new-feature-should-you-start-with-automation-or-by-inspecting-it-yourself/</id><summary type="html">&lt;p&gt;A fellow engineer submitted a question to internal mailing list. The gist is: a new feature is supposed to be released in a few months and there is no automation coverage for it. What should a person with a Quality Software Engineer title do? Should they test the feature &amp;#8220;manually …&lt;/p&gt;</summary><content type="html">&lt;p&gt;A fellow engineer submitted a question to internal mailing list. The gist is: a new feature is supposed to be released in a few months and there is no automation coverage for it. What should a person with a Quality Software Engineer title do? Should they test the feature &amp;#8220;manually&amp;#8221; and contribute automation later, risking it happens after a release? Or should they start writing automation right away, potentially postponing finding and reporting important&amp;nbsp;issues?&lt;/p&gt;
&lt;p&gt;My answer is&amp;nbsp;below.&lt;/p&gt;
&lt;p&gt;There are no simple recommendations and go-to answers that are going to be usually correct. It all&amp;nbsp;depends.&lt;/p&gt;
&lt;p&gt;First and foremost, what is the state of the feature in general? Are we still at the ideation phase, is any code already developed, does developer consider it done, did anyone else test it&amp;nbsp;already?&lt;/p&gt;
&lt;p&gt;Second, where does that feature fit in entire product? Is this completely new thing, or extension to something that product already does? Is this on the critical path of product usage? Did any customers ask for that, or did we internally decide it&amp;#8217;s a good idea? Is it going to be marked as tech preview in release notes or&amp;nbsp;not?&lt;/p&gt;
&lt;p&gt;Third, what is the social context of your team? Are you the only Software Quality Engineer, or is there a larger team? What is the workload for you and others? Who develops unit tests? And are there unit tests for this&amp;nbsp;feature?&lt;/p&gt;
&lt;p&gt;Fourth, how does that feature fit your automation suite? Can you easily extend existing framework to account for the new thing, or do you need to create completely new capabilities first? How much time do you think it&amp;#8217;s going to&amp;nbsp;take?&lt;/p&gt;
&lt;p&gt;As a tester, your main job is to provide accurate information to the team. Ideally, you would do that sooner rather than later. You should be able to identify risks and start the conversation on what the team is going to do with them. If you can, propose solutions and advocate for them. If you can and have time for that, implement solutions&amp;nbsp;yourself.&lt;/p&gt;
&lt;p&gt;Personally, I always start with understanding of the feature. What is the goal? How is it achieved? What workflow do we envision? Where are &amp;#8220;hot&amp;#8221; points where workflow can fail (i.e. due to unmet dependency)? Depending on the size of the feature, I would spend at least few hours ensuring that my understanding is correct and verifying my assumptions. More often than not, I will encounter questions that I can&amp;#8217;t find answers for, and if I am (un)lucky - that nobody thought about before. I will try to do that without looking at the implementation, to not guide my thinking and to avoid&amp;nbsp;biases.&lt;/p&gt;
&lt;p&gt;Then - or while doing above - I will create a list of test ideas. I will keep it very lightweight, maybe a sentence for each idea. Usually it&amp;#8217;s a mix of things I want to verify and things I want to find answers&amp;nbsp;for.&lt;/p&gt;
&lt;p&gt;At this point I will start thinking about the most effective ways of performing these tests and finding answers. Sometimes I will use tools to help me; sometimes these are going to be tools I create myself. At this point I don’t think about maintainability, performance, error handling etc. - it’s too early for that. Sometimes I will look into unit tests (if there are any) and see if the answer is already there. It may happen that I find unit test for a thing close enough to one I was interested in. In such case I will leverage it. That means modifying the test or creating new one based on one that I found. I might try to submit a &lt;span class="caps"&gt;PR&lt;/span&gt; with this new or modified test, so it becomes a part of unit test suite, but usually it&amp;#8217;s not my first&amp;nbsp;priority.&lt;/p&gt;
&lt;p&gt;As I go through my list, usually I encounter things that are interesting, puzzling or noteworthy for other reasons. Often they trigger new test ideas that I did not think about earlier. I will perform them right away or add to the list, to check&amp;nbsp;later.&lt;/p&gt;
&lt;p&gt;When I decide I have seen enough, it&amp;#8217;s time to think what tests seem important enough to include in future regression runs. It&amp;#8217;s the act of balancing sometimes conflicting constraints - what value does the test bring in the great scheme of things? How costly it is to develop? How fast can it run? Where is the right place to run it? Usually only a fraction of test ideas are going to make it into regression&amp;nbsp;suite.&lt;/p&gt;
&lt;p&gt;Finally, obviously, I need to implement these regression tests. I will start with most important first and look for ways that other people could help with&amp;nbsp;that.&lt;/p&gt;
&lt;p&gt;So, to answer your question: neither of the two. Look for things that matter. Identify real problems and focus on letting others know about them quickly. Use tools to aid you, but remember that tool is only here to support you. Don&amp;#8217;t repeat the work that developers have already done. See if you can leverage the work of other team&amp;nbsp;members.&lt;/p&gt;</content><category term="Blog"/><category term="planet AST"/><category term="planet MoT"/><category term="testing"/></entry><entry><title>Understanding Linux virtualization stack</title><link href="https://mirekdlugosz.com/blog/2024/understanding-linux-virtualization-stack/" rel="alternate"/><published>2024-11-12T23:37:46+01:00</published><updated>2024-11-12T23:37:46+01:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2024-11-12:/blog/2024/understanding-linux-virtualization-stack/</id><summary type="html">&lt;p&gt;I find Linux virtualization stack confusing. &lt;span class="caps"&gt;KVM&lt;/span&gt;? libvirt? &lt;span class="caps"&gt;QEMU&lt;/span&gt;? Xen? What does that even&amp;nbsp;mean?&lt;/p&gt;
&lt;p&gt;This post is my attempt at making sense of that all. I don&amp;#8217;t claim it to be correct, but that&amp;#8217;s the way I understand it. If I badly missed a mark somewhere, please …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I find Linux virtualization stack confusing. &lt;span class="caps"&gt;KVM&lt;/span&gt;? libvirt? &lt;span class="caps"&gt;QEMU&lt;/span&gt;? Xen? What does that even&amp;nbsp;mean?&lt;/p&gt;
&lt;p&gt;This post is my attempt at making sense of that all. I don&amp;#8217;t claim it to be correct, but that&amp;#8217;s the way I understand it. If I badly missed a mark somewhere, please &lt;a href="https://mirekdlugosz.com/contact.html"&gt;reach out and tell me how wrong I am&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="virtualization-primer"&gt;&lt;a class="toclink" href="#virtualization-primer"&gt;Virtualization&amp;nbsp;primer&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Virtual machine is a box inside which programs think they are running on different hardware and operating system. That box is like a full computer, running inside a computer. Virtualization is process of creating these boxes. Virtual machine is called guest, while operating system that runs virtual machine is called&amp;nbsp;host.&lt;/p&gt;
&lt;p&gt;Two main reasons to virtualize are security and resource management. Security - because if virtual machine is done correctly, then program inside it won&amp;#8217;t even know it&amp;#8217;s inside a virtual machine, and that means there&amp;#8217;s a good chance it won&amp;#8217;t escape to interfere with other programs. Resource management - so you can buy huge server and assign slice of resources to each box as you see fit, and dynamically change it as your needs&amp;nbsp;shift.&lt;/p&gt;
&lt;p&gt;Virtual machine pretends to be the entire computer, but most discussions revolve around &lt;span class="caps"&gt;CPU&lt;/span&gt;. Each &lt;span class="caps"&gt;CPU&lt;/span&gt; architecture has a specific set of instructions, and programs generally target only one of them. It is technically possible to translate instructions from one architecture to another, but that&amp;#8217;s usually slow. Most modern CPUs provide extensions that allow virtual machines to run at speed comparable to non-virtualized installations. Using these extensions is only possible when the guest and the host target the same &lt;span class="caps"&gt;CPU&lt;/span&gt;&amp;nbsp;architecture.&lt;/p&gt;
&lt;p&gt;Conceptually, there is no significant difference between virtualization and emulation. Practically, emulation usually refers to guest thinking it has different &lt;span class="caps"&gt;CPU&lt;/span&gt; architecture than host, and to virtual machine pretending to be some very specific physical hardware - like a video game console. Virtualization in turn usually refers to specific case where guest thinks it has the same &lt;span class="caps"&gt;CPU&lt;/span&gt; architecture as the&amp;nbsp;host.&lt;/p&gt;
&lt;h2 id="kvm"&gt;&lt;a class="toclink" href="#kvm"&gt;&lt;span class="caps"&gt;KVM&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;span class="caps"&gt;KVM&lt;/span&gt; is part of the kernel, responsible for talking with hardware. As I mentioned before, most modern CPUs have special extensions that allow guests to achieve near-native performance. &lt;span class="caps"&gt;KVM&lt;/span&gt; makes it possible for Linux host to use these&amp;nbsp;extensions.&lt;/p&gt;
&lt;h2 id="qemu"&gt;&lt;a class="toclink" href="#qemu"&gt;&lt;span class="caps"&gt;QEMU&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;span class="caps"&gt;QEMU&lt;/span&gt; is weird, because it&amp;#8217;s multiple different&amp;nbsp;things.&lt;/p&gt;
&lt;p&gt;First, it can run virtual machines and pass instructions from guest to &lt;span class="caps"&gt;KVM&lt;/span&gt;, where they will be handled by virtualization &lt;span class="caps"&gt;CPU&lt;/span&gt; extension. So you can think about &lt;span class="caps"&gt;QEMU&lt;/span&gt; as user space component for &lt;span class="caps"&gt;KVM&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;Second, it can run virtual machines while emulating different &lt;span class="caps"&gt;CPU&lt;/span&gt; architecture. So you can think about &lt;span class="caps"&gt;QEMU&lt;/span&gt; as user space virtualization solution, which can run without &lt;span class="caps"&gt;KVM&lt;/span&gt; at&amp;nbsp;all.&lt;/p&gt;
&lt;p&gt;Third, it can run programs targeting one &lt;span class="caps"&gt;CPU&lt;/span&gt; architecture on a computer with another &lt;span class="caps"&gt;CPU&lt;/span&gt; architecture. It&amp;#8217;s like lightweight virtualization, where only single program is&amp;nbsp;virtualized.&lt;/p&gt;
&lt;p&gt;Finally, it is set of standalone utilities related to virtualization, but not directly tied to anything in particular. Most of these programs work with disk image files in some&amp;nbsp;way.&lt;/p&gt;
&lt;p&gt;&lt;span class="caps"&gt;QEMU&lt;/span&gt; is probably the hardest part to understand, because it can do things that other parts of the stack are responsible for. This results in some overlap between different components and encountering program names in contexts where you would not expect&amp;nbsp;them.&lt;/p&gt;
&lt;p&gt;I &lt;em&gt;think&lt;/em&gt; this overlap is mostly a historical artifact. The oldest commit in &lt;span class="caps"&gt;QEMU&lt;/span&gt; git repository is dated 2003, while &lt;span class="caps"&gt;KVM&lt;/span&gt; was included in Linux kernel in 2007. So it seems to me that &lt;span class="caps"&gt;QEMU&lt;/span&gt; was originally intended as software virtualization solution that did not depend on any hardware or kernel driver. Later Linux gained drivers for virtualization &lt;span class="caps"&gt;CPU&lt;/span&gt; extensions, but they were useless without something in user space that could work with them. So instead of creating completely new thing, &lt;span class="caps"&gt;QEMU&lt;/span&gt; was extended to work with &lt;span class="caps"&gt;KVM&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;Technically, the journey towards understanding Linux virtualization stack could end here - you &lt;em&gt;can&lt;/em&gt; use &lt;span class="caps"&gt;QEMU&lt;/span&gt; to create, start, stop and modify virtual machines. But &lt;span class="caps"&gt;QEMU&lt;/span&gt; commands tend to be verbose and long, so people rarely use it&amp;nbsp;directly.&lt;/p&gt;
&lt;h2 id="libvirt"&gt;&lt;a class="toclink" href="#libvirt"&gt;libvirt&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I have ignored that so far, but &lt;span class="caps"&gt;KVM&lt;/span&gt; is not the only virtualization driver in kernel. There are many, some of them&amp;nbsp;closed-source.&lt;/p&gt;
&lt;p&gt;libvirt is intended as unified layer that abstracts virtual machine management for application developers. So if you would like your application to create, start, stop or delete virtual machine, you don&amp;#8217;t have to support each of these operations on &lt;span class="caps"&gt;KVM&lt;/span&gt;, &lt;span class="caps"&gt;QEMU&lt;/span&gt;, Xen and VirtualBox - you can just support libvirt and trust it will do the right&amp;nbsp;thing.&lt;/p&gt;
&lt;p&gt;libvirt doesn&amp;#8217;t do any work directly, and instead asks other programs to do it. System administrator is responsible for setting up the host and ensuring virtualization may work. This makes libvirt great for application developers, who can forget about details of different solutions; but most of libvirt users are in less fortunate position, because they have to take a role of system&amp;nbsp;administrator.&lt;/p&gt;
&lt;p&gt;libvirt will talk with &lt;span class="caps"&gt;QEMU&lt;/span&gt;, which in turn may pass &lt;span class="caps"&gt;CPU&lt;/span&gt; instructions to &lt;span class="caps"&gt;KVM&lt;/span&gt;. Some sources online claim that libvirt can talk with &lt;span class="caps"&gt;KVM&lt;/span&gt; directly. libvirt itself perpetuates this confusion, as &lt;span class="caps"&gt;KVM&lt;/span&gt; is listed alongside &lt;span class="caps"&gt;QEMU&lt;/span&gt; on the main page, giving the impression they are two equivalent components. But detailed documentation page makes it clear - libvirt only talks with &lt;span class="caps"&gt;QEMU&lt;/span&gt;. It can detect &lt;span class="caps"&gt;KVM&lt;/span&gt; and allow user to create &amp;#8220;hardware accelerated guests&amp;#8221;, but that case is still handled through &lt;span class="caps"&gt;QEMU&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;It&amp;#8217;s also worth noting that libvirt supports multiple virtualization drivers across multiple operating systems, including FreeBSD and&amp;nbsp;macOS.&lt;/p&gt;
&lt;h2 id="virsh"&gt;&lt;a class="toclink" href="#virsh"&gt;virsh&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;virsh is a command line interface for libvirt. That&amp;#8217;s it, there&amp;#8217;s nothing more to it. If it was created today, it might have been called&amp;nbsp;libvirtctl.&lt;/p&gt;
&lt;h2 id="vagrant"&gt;&lt;a class="toclink" href="#vagrant"&gt;Vagrant&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Vagrant abstracts virtual machine management across operating systems. It&amp;#8217;s similar to libvirt, in the way that it allows application developers to target Vagrant and leave all the details to someone else. And just like libvirt, Vagrant doesn&amp;#8217;t do anything on its own, but pushes all the work to tools lower in the&amp;nbsp;stack.&lt;/p&gt;
&lt;p&gt;Vagrant primary audience is software developers. It allows to succinctly define multiple virtual machines, and then start and provision them all with a single command. These machines are usually considered disposable - they might be deleted and re-created multiple times a day. If you want to manually modify the virtual machine and keep these changes for longer period of time, Vagrant is probably not a tool for&amp;nbsp;you.&lt;/p&gt;
&lt;p&gt;On Linux, Vagrant usually works with libvirt, but may also work with VirtualBox or&amp;nbsp;Xen.&lt;/p&gt;
&lt;h2 id="virtualbox"&gt;&lt;a class="toclink" href="#virtualbox"&gt;VirtualBox&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;VirtualBox is full virtualization solution. It consists of kernel driver and user space programs, including one with graphical interface. You can think of VirtualBox as something parallel to all that we&amp;#8217;ve discussed so&amp;nbsp;far.&lt;/p&gt;
&lt;p&gt;The main reason to use VirtualBox is ease of use. It offers simpler mental model for virtualization - you just install VirtualBox, start VirtualBox &lt;span class="caps"&gt;UI&lt;/span&gt; and create and run virtual machines. While it does require a kernel module, and kernel is notoriously bad at breaking modules that are not in tree, VirtualBox has a lot of tooling and integrations that will ensure that module is built every time you install or update kernel image. On distributions like Ubuntu you don&amp;#8217;t have to think about this at&amp;nbsp;all.&lt;/p&gt;
&lt;p&gt;Technically speaking, libvirt supports VirtualBox, so you can use libvirt and virsh to manage VirtualBox machines. In practice that seems to be&amp;nbsp;rare.&lt;/p&gt;
&lt;h2 id="xen"&gt;&lt;a class="toclink" href="#xen"&gt;Xen&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Xen is another virtualization stack. You can think about it as something parallel to &lt;span class="caps"&gt;KVM&lt;/span&gt;/&lt;span class="caps"&gt;QEMU&lt;/span&gt;/libvirt and VirtualBox. What sets Xen apart is that instead of running &lt;em&gt;inside&lt;/em&gt; your operating system, it runs &lt;em&gt;below&lt;/em&gt; your operating&amp;nbsp;system.&lt;/p&gt;
&lt;p&gt;Xen manages scheduling, interrupts and timers - that is, it manages who gets access to computer resources, when, and for how long. Xen is the very first thing that starts when you boot your&amp;nbsp;computer.&lt;/p&gt;
&lt;p&gt;But Xen is not an operating system. So right after starting, it starts the guest with one. That guest is special, because it provides drivers to hardware and is the only one with a privilege to talk with Xen. Only through that special guest virtual machine you can create other virtual machines, assign them resources&amp;nbsp;etc.&lt;/p&gt;
&lt;p&gt;Xen is one of the oldest virtualization solutions, with first release back in 2003. However, it was included in kernel only in 2010. So while many people prefer Xen and think favorably of its architecture, it seems to lost the popularity contest to &lt;span class="caps"&gt;KVM&lt;/span&gt; and &lt;span class="caps"&gt;QEMU&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;Xen provides the tools required to manage guest virtual machines, but you can also manage them through&amp;nbsp;libvirt.&lt;/p&gt;
&lt;h2 id="summary"&gt;&lt;a class="toclink" href="#summary"&gt;Summary&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I thought to put a visual aid in place of summary. Each column represents a possible stack - you generally want to pick one of them. Items with dashed line style are optional - you can use them, but don&amp;#8217;t have to. Click to see the bigger&amp;nbsp;image.&lt;/p&gt;
&lt;figure&gt;
        &lt;a href="https://mirekdlugosz.com/blog/2024/understanding-linux-virtualization-stack/linux-virtualization-stack/linux-virtualization-stack.jpg"&gt;
            &lt;img src="https://mirekdlugosz.com/blog/2024/understanding-linux-virtualization-stack/linux-virtualization-stack/linux-virtualization-stack-min.jpg" title="Overview of possible Linux virtualization stacks" alt="Overview of possible Linux virtualization stacks" loading="lazy"&gt;
        &lt;/a&gt;
        
    &lt;/figure&gt;</content><category term="Blog"/><category term="Linux"/><category term="planet AST"/><category term="planet MoT"/><category term="planet Python"/></entry><entry><title>“Software Testing Strategies” by Heusser and Larsen - review</title><link href="https://mirekdlugosz.com/blog/2024/software-testing-strategies-by-heusser-and-larsen-review/" rel="alternate"/><published>2024-10-22T19:54:29+02:00</published><updated>2024-10-22T19:54:29+02:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2024-10-22:/blog/2024/software-testing-strategies-by-heusser-and-larsen-review/</id><summary type="html">&lt;p&gt;This is the review of a book &amp;#8220;Software Testing Strategies. A testing guide for the 2020s&amp;#8221; by Matthew Heusser and Michael Larsen. The book was published in December 2023 by Packt. You can buy the ebook or printed copy directly from the publisher, at Amazon and perhaps a number of …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This is the review of a book &amp;#8220;Software Testing Strategies. A testing guide for the 2020s&amp;#8221; by Matthew Heusser and Michael Larsen. The book was published in December 2023 by Packt. You can buy the ebook or printed copy directly from the publisher, at Amazon and perhaps a number of other&amp;nbsp;places.&lt;/p&gt;
&lt;p&gt;I admit I was a bit on the fence since I first heard about the book. The title sounds very promising and I know authors were involved in &lt;a href="https://associationforsoftwaretesting.org/"&gt;&lt;span class="caps"&gt;AST&lt;/span&gt;&lt;/a&gt; and context-driven testing. But the price seems a bit high in this part of the world. Eventually I took the plunge and bought a copy in March. I finished reading after 5 or 6 weeks. I wrote this review in&amp;nbsp;April.&lt;/p&gt;
&lt;h2 id="things-that-i-liked"&gt;&lt;a class="toclink" href="#things-that-i-liked"&gt;Things that I&amp;nbsp;liked&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The book is full of stories, examples and experience reports. You can easily tell that authors have actual experience with things that they talk about. This is a welcome change from a number of books and blog posts that are superficial and read as if authors had only theoretical knowledge of a thing they cover, if any. Overall, that makes for an easier reading and adds credibility. I especially appreciate that authors did not shy away from talking about their mistakes, blunders and errors. Too many experience reports are vanity in&amp;nbsp;disguise.&lt;/p&gt;
&lt;p&gt;Case studies in chapter 12 are wonderful. Testing community needs so much more of these. I dream that one day we might have a testing-focused &lt;a href="https://commoncog.com/how-note-taking-can-help-you-become-an-expert/"&gt;case library&lt;/a&gt;. I hope to write about this more in the future, and I would be more than happy to join a group that is working towards that goal. But until these dreams and hopes come true, chapter 12 is one of the closest things available to international&amp;nbsp;audience.&lt;/p&gt;
&lt;p&gt;There were things I have learned. Pairwise testing sounds like something I should have known, but somehow I missed it so far. Model-driven testing is something I encountered recently, but it was called by a different name. It also has a surprising connection to high-volume automation I have implemented in one of the&amp;nbsp;projects.&lt;/p&gt;
&lt;p&gt;I found it thought-provoking to introduce two new schools when discussing the schools of testing in chapter 15 - &amp;#8220;The Agile school&amp;#8221; and &amp;#8220;The DevOps or &lt;span class="caps"&gt;CD&lt;/span&gt; school&amp;#8221;. The idea of testing schools themselves is a bit dated, caused a lot of controversy at the time and is now largely rejected by the community. So I&amp;#8217;m not sure if it&amp;#8217;s a good thing to include it in the book, but I do appreciate the effort to modernize&amp;nbsp;it.&lt;/p&gt;
&lt;p&gt;I really enjoyed the description of testing work in the very first chapter. It did an excellent job at capturing the thrill of exploration and the joy of learning something new. Even though I am fortunate enough to enjoy these feelings most of the time at my daily job, I still found that part inspiring. It has this rare and precious effect that it makes you want to put the book down and go test&amp;nbsp;something.&lt;/p&gt;
&lt;h2 id="things-i-did-not-like"&gt;&lt;a class="toclink" href="#things-i-did-not-like"&gt;Things I did not&amp;nbsp;like&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I don&amp;#8217;t think the title suits the content well. I have expected something similar to &amp;#8220;Design Patterns&amp;#8221; by the Gang of Four - a list of established and emerging strategies, each with a description, probably an example, and a summary that says when a strategy can be applied beneficially, and when it&amp;#8217;s better avoided. This book is not&amp;nbsp;that.&lt;/p&gt;
&lt;p&gt;The book is all over the place. It touches a huge number of topics, but dives deep into only a handful of them. Sometimes it&amp;#8217;s hard to understand why certain things were included and how they relate to everything else. This makes it challenging to decide which topics are worth studying more and which can be ignored for&amp;nbsp;now.&lt;/p&gt;
&lt;p&gt;It&amp;#8217;s unclear to me what level of experience is the reader assumed to have. For example, hexagonal architecture is introduced as early as chapter 3. But this is relatively advanced topic from software architecture area - something that most testers don&amp;#8217;t really encounter, and developers start to think about as they prepare to move to a senior role, or later. The way it&amp;#8217;s being described might make you think this is how most software looks like internally, which could not be further from&amp;nbsp;truth.&lt;/p&gt;
&lt;p&gt;The choice of software covered is baffling at times. Chapter 5 mentions couple of continuous integration systems. Somehow CircleCI and Travis &lt;span class="caps"&gt;CI&lt;/span&gt; were included, but GitHub Actions was not. The same chapter recommends Apache OpenOffice, which at this point of time is straight negligence. Apache OpenOffice is abandonware that barely manages to release a few bugfixes once a year or so. All the community has moved on to &lt;a href="https://libreoffice.org"&gt;LibreOffice&lt;/a&gt;, which sees 2 major releases every year and monthly bugfix releases. If you want to recommend free and open source office suite, LibreOffice is the only one in&amp;nbsp;town.&lt;/p&gt;
&lt;p&gt;There are occasional mistakes and typos in the text. Duplicated word in one place, sentence beginning by lowercase letter in another. These are mostly negligible. The one exception is in the chapter 13: &amp;#8220;division by zero and interpreted code crashes tend to show up as 400 errors&amp;#8221;. I think this is a typo, but one that novices will not catch and instead learn the wrong thing. 400 errors are client-side errors; division by zero and crashes in interpreted code are most likely going to show up as 500 errors, server-side problems. I don&amp;#8217;t really blame the authors for this and other mistakes, but it&amp;#8217;s clear that the editor assigned by the publisher could do a better job. That&amp;#8217;s&amp;nbsp;unfortunate.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;&lt;a class="toclink" href="#conclusion"&gt;Conclusion&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I want to like this book. I really do. But I am clearly not a target audience. So I&amp;#8217;m thinking: to whom could I recommend&amp;nbsp;it?&lt;/p&gt;
&lt;p&gt;I think this might be a good book for someone early in their career, probably someone with 6 months up to a few years of experience. To readers with less experience I would recommend &amp;#8220;Perfect Software&amp;#8221; by Jerry Weinberg instead. More experienced folks might find &amp;#8220;Lessons Learned in Software Testing&amp;#8221; by Kaner, Bach and Pettichord to be more valuable. But there is a sweet spot between these two prominent works that this book seems to&amp;nbsp;fill.&lt;/p&gt;
&lt;p&gt;Perhaps this book can also help people who have been in testing for a very long time, settled a bit, feel they no longer keep up with the Joneses and want to change that. They won&amp;#8217;t learn ins and outs of everything in the current landscape of software development, but will get the useful overview. They will encounter all the most important ideas, and some buzzwords&amp;nbsp;too.&lt;/p&gt;
&lt;p&gt;That might be a bit of a stretch, but I guess this can also be an alright book for people who consider a career in &lt;span class="caps"&gt;IT&lt;/span&gt;, and are looking for single book that would give them the general overview of how the work looks like today. Ironically, these people might want to skip some of the parts that discuss testing in&amp;nbsp;depth.&lt;/p&gt;</content><category term="Blog"/><category term="planet AST"/><category term="planet MoT"/><category term="testing"/></entry><entry><title>AST Board of Directors elections are on!</title><link href="https://mirekdlugosz.com/blog/2024/ast-board-of-directors-elections-are-on/" rel="alternate"/><published>2024-07-08T21:38:06+02:00</published><updated>2024-07-08T21:38:06+02:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2024-07-08:/blog/2024/ast-board-of-directors-elections-are-on/</id><summary type="html">&lt;p&gt;The Association for Software Testing yearly election process has started. We are looking for three people to join the&amp;nbsp;Board.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://associationforsoftwaretesting.org/2024/07/01/ast-nominations-are-open/"&gt;Official announcement on &lt;span class="caps"&gt;AST&lt;/span&gt; site has all the details, including dates and formal requirements&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you aren&amp;#8217;t sure if you would be a good fit, you have any questions …&lt;/p&gt;</summary><content type="html">&lt;p&gt;The Association for Software Testing yearly election process has started. We are looking for three people to join the&amp;nbsp;Board.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://associationforsoftwaretesting.org/2024/07/01/ast-nominations-are-open/"&gt;Official announcement on &lt;span class="caps"&gt;AST&lt;/span&gt; site has all the details, including dates and formal requirements&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you aren&amp;#8217;t sure if you would be a good fit, you have any questions about the Board, Association or the elections, or you are looking for someone from within the organization to support your nomination, I am more than happy to help - just &lt;a href="https://mirekdlugosz.com/contact.html"&gt;reach out using the usual channels&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And if you can&amp;#8217;t run for election because you are not a member yet, now is the perfect time to join - just in time to meet formal requirements for the next year. Head on to &lt;a href="https://associationforsoftwaretesting.org/about-the-ast-a-professional-body-for-testers/membership/become-a-member-of-the-the-association-for-software-testing/"&gt;&amp;#8220;Become a Member&amp;#8221; page on &lt;span class="caps"&gt;AST&lt;/span&gt; website&lt;/a&gt;.&lt;/p&gt;</content><category term="Blog"/><category term="AST"/><category term="planet AST"/><category term="planet MoT"/></entry><entry><title>How can I possibly test “all the stuff” every iteration?</title><link href="https://mirekdlugosz.com/blog/2024/how-can-i-possibly-test-all-the-stuff-every-iteration/" rel="alternate"/><published>2024-06-20T22:49:44+02:00</published><updated>2024-06-20T22:49:44+02:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2024-06-20:/blog/2024/how-can-i-possibly-test-all-the-stuff-every-iteration/</id><summary type="html">&lt;p&gt;The Association for Software Testing is crowd-sourcing a book, &lt;a href="https://associationforsoftwaretesting.org/navigating-the-world-as-a-context-driven-tester-book/"&gt;&lt;em&gt;Navigating the World as a Context-Driven Tester&lt;/em&gt;&lt;/a&gt;. The book is edited by &lt;a href="https://therockertester.wordpress.com/"&gt;Lee Hawkins&lt;/a&gt;, who posts questions on &lt;a href="https://sw-development-is.social/web/@AST"&gt;Mastodon&lt;/a&gt;, &lt;a href="https://www.linkedin.com/company/association-for-software-testing/"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://bsky.app/profile/ast-news.bsky.social"&gt;BlueSky&lt;/a&gt;, &lt;a href="https://twitter.com/AST_News"&gt;Twitter&lt;/a&gt;, &lt;a href="https://associationforsoftwaretesting.org/2016/11/13/ast-members-slack/"&gt;Slack&lt;/a&gt;, and &lt;a href="http://eepurl.com/tCFsn"&gt;the &lt;span class="caps"&gt;AST&lt;/span&gt; mailing list&lt;/a&gt;. You don&amp;#8217;t have to be a member to give your answer to …&lt;/p&gt;</summary><content type="html">&lt;p&gt;The Association for Software Testing is crowd-sourcing a book, &lt;a href="https://associationforsoftwaretesting.org/navigating-the-world-as-a-context-driven-tester-book/"&gt;&lt;em&gt;Navigating the World as a Context-Driven Tester&lt;/em&gt;&lt;/a&gt;. The book is edited by &lt;a href="https://therockertester.wordpress.com/"&gt;Lee Hawkins&lt;/a&gt;, who posts questions on &lt;a href="https://sw-development-is.social/web/@AST"&gt;Mastodon&lt;/a&gt;, &lt;a href="https://www.linkedin.com/company/association-for-software-testing/"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://bsky.app/profile/ast-news.bsky.social"&gt;BlueSky&lt;/a&gt;, &lt;a href="https://twitter.com/AST_News"&gt;Twitter&lt;/a&gt;, &lt;a href="https://associationforsoftwaretesting.org/2016/11/13/ast-members-slack/"&gt;Slack&lt;/a&gt;, and &lt;a href="http://eepurl.com/tCFsn"&gt;the &lt;span class="caps"&gt;AST&lt;/span&gt; mailing list&lt;/a&gt;. You don&amp;#8217;t have to be a member to give your answer to the most recent prompt, &amp;#8220;How can I possibly test »all the stuff« every iteration?&amp;#8221;. Mine is&amp;nbsp;below.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Depends on what you mean by &amp;#8220;all the&amp;nbsp;stuff&amp;#8221;.&lt;/p&gt;
&lt;p&gt;You might mean the full regression testing of entire application, including all the features, negative paths, edge cases etc. And you are right - you can&amp;#8217;t possibly do all these things every iteration, maybe except for some very small&amp;nbsp;products.&lt;/p&gt;
&lt;p&gt;But the thing is: you shouldn&amp;#8217;t &lt;em&gt;do&lt;/em&gt; this kind of comprehensive testing in every iteration, you shouldn&amp;#8217;t &lt;em&gt;feel the need&lt;/em&gt; to do it, and you shouldn&amp;#8217;t be &lt;em&gt;asked&lt;/em&gt; to do&amp;nbsp;it.&lt;/p&gt;
&lt;p&gt;The iteration is the concept from the agile software development world - in the model where software is developed for extended period of time and delivered once, at the end, the word &amp;#8220;iteration&amp;#8221; has no meaning. The whole point of iterating over software during the development is to introduce small, but valuable change and deliver it to the users. Since the change is small, most of the product is the same as it was and therefore does not need to undergo comprehensive verification again. It is assumed that it was already tested in the&amp;nbsp;past.&lt;/p&gt;
&lt;p&gt;But maybe changes have broader scope than it looks like, or they introduce unintended consequences in other parts of the program. These are fair concerns. However, most changes introduced by developers don&amp;#8217;t impact the entire system and you should not assume by default that they might. It definitely pays off to identify code with potentially large impact and make informed decision about scope of testing needed during the&amp;nbsp;iteration.&lt;/p&gt;
&lt;p&gt;It&amp;#8217;s worth adding that if the team routinely finds itself in the situation that changes ripple through the entire system and comprehensive testing is necessary, then that&amp;#8217;s a sign of deeper issues to tackle. It might mean that the architecture of the system is wrong and the team should seriously consider paying off the technical debt. Or maybe the software is more of a prototype, proof of concept, than the solution. In that case, perhaps comprehensive testing is not needed until requirements are better understood by the team and stakeholders can green-light one&amp;nbsp;approach.&lt;/p&gt;
&lt;p&gt;Many testers struggle with working in this kind of environment. They see their role as a last line of defense between a possibly buggy software and its users. The idea that users might get a software that was not comprehensively tested and verified make them feel&amp;nbsp;uneasy.&lt;/p&gt;
&lt;p&gt;If you are one of them, you need to understand that this kind of risk is calculated and intrinsic to software development model that values short iteration and frequent delivery. These ideas reinforce each other. The product is not comprehensively tested, which means there might be bugs, mistakes and problems that won&amp;#8217;t be caught before users encounter them. But &lt;em&gt;because&lt;/em&gt; the product is not comprehensively tested, it can be released faster. When the team learns about problems in the software, they can be fixed and a new release may be&amp;nbsp;made.&lt;/p&gt;
&lt;p&gt;If this argument does not convince you, I would urge to just give it a try. Acknowledge that you personally are not sold to this approach, but don&amp;#8217;t block it if this is what the team wants to do. Keep your reservations to yourself for few releases and see what happens. You might learn that the product is not, in fact, significantly lower quality than before. And if it turns out the quality has lowered, then you will have hard empirical data to show to the team. Most people will agree these results are undesirable and some kind of change is&amp;nbsp;needed.&lt;/p&gt;
&lt;p&gt;Finally, you should not be asked to do comprehensive testing during each iteration. Personally, I don&amp;#8217;t have much experience in that regard - I have never been in a team that would attempt that. While I can&amp;#8217;t say what things might work in such situation, I guess the first thing to do is understanding the position such request is coming from. Perhaps stakeholders need to be taught the tradeoffs of iterations; or perhaps the team needs to adopt to more cautious software development models at the cost of&amp;nbsp;speed.&lt;/p&gt;
&lt;p&gt;I have not mentioned automation yet, which contributes to all of the points so far. Your work as a tester should be supported by automation that runs at least once a day and delivers up-to-date and reliable information about the state of the product. While automation has limits and can&amp;#8217;t check everything, it can check &lt;em&gt;some things&lt;/em&gt;. You should understand what automation can and can&amp;#8217;t do in your project, so you can make an informed decisions about which features and paths you don&amp;#8217;t need to spend much time on. You should feel confident in automation results. And when someone asks you to repeatedly perform specific tests, you might use that as opportunity to discuss spending some time on improving automation&amp;nbsp;instead.&lt;/p&gt;
&lt;p&gt;At the beginning of this article I said that the answer depends on the meaning of &amp;#8220;all the stuff&amp;#8221;. It might mean comprehensive testing, which I focused on so far. But it might also mean all the things that have changed during the&amp;nbsp;iteration.&lt;/p&gt;
&lt;p&gt;It does happen there&amp;#8217;s so much work done by developers that testers in the team are swamped. I was in that situation not too long ago, and what did work for us was pushing for whole team ownership. Most of development tasks don&amp;#8217;t need to be tested or verified by a dedicated tester - they only need to be tested by someone who did not write the code. In our case, all we had to do was signalling the problem during standup meeting, and rest of the team quickly came up with a solution - that some of developers should stop taking new work and instead focus on testing things that are already done. Having clearly spelled out definition of done and acceptance criteria for each ticket definitely&amp;nbsp;helps.&lt;/p&gt;
&lt;p&gt;However, the best way to deal with that problem is to actively avoid it. In general, as a tester, try to test changes as soon as possible. Don&amp;#8217;t wait until the end of the iteration. If the team practices &lt;span class="caps"&gt;PR&lt;/span&gt;-based workflows, you can try testing the development build created before the code is merged. It helps to have the &lt;span class="caps"&gt;CI&lt;/span&gt; system that builds artifacts for each &lt;span class="caps"&gt;PR&lt;/span&gt;, but alternatively you can set up the build environment on your own machine. For exceptionally large features, give feedback to developer before code is written, and encourage early sharing of code and ideas. Daily standup, if there is one, is a good opportunity to show desire of this kind of conversations. You can also consider reaching out to developer and discuss things in&amp;nbsp;private.&lt;/p&gt;
&lt;p&gt;It might happen that despite using all the suggestions above, testers are still routinely swamped with work at the end of the sprint. In my experience, this is more likely to happen in teams that try to strictly follow rigid scrum structure, with iterations taking a week or two. Since work is assigned at the beginning of the sprint, there is a period of time when developers are focused on their work, and testers don&amp;#8217;t have many things to do. That dynamic changes towards the end of the sprint. In that case, I would highly encourage to bring this up during the retrospective meeting. Most of the people will agree that this is a systematic problem that is not sustainable in the long term. As usual with self-organizing teams, it&amp;#8217;s up to the team to come up with a solution. Just remember to monitor the situation and raise this topic again if there is no visible&amp;nbsp;improvement.&lt;/p&gt;</content><category term="Blog"/><category term="AST"/><category term="planet AST"/><category term="planet MoT"/><category term="testing"/></entry><entry><title>Association for Software Testing Board of Directors report - April 2024</title><link href="https://mirekdlugosz.com/blog/2024/ast-bod-report-april-2024/" rel="alternate"/><published>2024-05-02T23:56:19+02:00</published><updated>2024-05-02T23:56:19+02:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2024-05-02:/blog/2024/ast-bod-report-april-2024/</id><summary type="html">&lt;p&gt;In my &lt;a href="https://associationforsoftwaretesting.org/about-the-ast-a-professional-body-for-testers/board-of-directors/election-proceedings/2023-election/2023-election-mirek-dlugosz/"&gt;Association for Software Testing 2023 elections proceedings&lt;/a&gt; I advocated for transparency. In the spirit of practising what I preach, this blog post summarizes my work for &lt;span class="caps"&gt;AST&lt;/span&gt; in April&amp;nbsp;2024.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Found a way to send messages to &lt;a href="https://bsky.app/profile/ast-news.bsky.social"&gt;BlueSky&lt;/a&gt; and &lt;a href="https://sw-development-is.social/web/@AST"&gt;Mastodon&lt;/a&gt; from&amp;nbsp;Zapier.&lt;/li&gt;
&lt;li&gt;Met with Chris and Gwen to …&lt;/li&gt;&lt;/ul&gt;</summary><content type="html">&lt;p&gt;In my &lt;a href="https://associationforsoftwaretesting.org/about-the-ast-a-professional-body-for-testers/board-of-directors/election-proceedings/2023-election/2023-election-mirek-dlugosz/"&gt;Association for Software Testing 2023 elections proceedings&lt;/a&gt; I advocated for transparency. In the spirit of practising what I preach, this blog post summarizes my work for &lt;span class="caps"&gt;AST&lt;/span&gt; in April&amp;nbsp;2024.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Found a way to send messages to &lt;a href="https://bsky.app/profile/ast-news.bsky.social"&gt;BlueSky&lt;/a&gt; and &lt;a href="https://sw-development-is.social/web/@AST"&gt;Mastodon&lt;/a&gt; from&amp;nbsp;Zapier.&lt;/li&gt;
&lt;li&gt;Met with Chris and Gwen to discuss remaining items from Phone.com migration (see &lt;a href="https://mirekdlugosz.com/blog/2024/ast-bod-report-january-2024/"&gt;January report&lt;/a&gt;). I hope to finally close that in&amp;nbsp;May.&lt;/li&gt;
&lt;li&gt;Removed old, no longer used passwords from shared LastPass&amp;nbsp;folders.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Integration of Zapier with BlueSky and Mastodon deserve a bit longer&amp;nbsp;explanation.&lt;/p&gt;
&lt;p&gt;There are two sources of our automatic messages - we periodically re-publish posts and videos from our archives, and we forward &lt;a href="https://associationforsoftwaretesting.org/about-the-ast-a-professional-body-for-testers/membership/blog-syndication/"&gt;blog posts from our members&lt;/a&gt;. For a longest time, we published that on &lt;a href="https://www.facebook.com/profile.php?id=100064365177059"&gt;Facebook&lt;/a&gt;, &lt;a href="https://www.linkedin.com/company/association-for-software-testing/"&gt;LinkedIn&lt;/a&gt; and &lt;a href="https://twitter.com/AST_News"&gt;Twitter&lt;/a&gt;. Last year Twitter announced they will require paid subscription to access &lt;span class="caps"&gt;API&lt;/span&gt;, which in turn rendered most existing automations useless. We had to turn off&amp;nbsp;ours.&lt;/p&gt;
&lt;p&gt;BlueSky and Mastodon quickly rose to prominence as possible Twitter replacements, so we created new accounts there. However, Zapier did not support them natively. In fact, it still does&amp;nbsp;not.&lt;/p&gt;
&lt;p&gt;But after a bit of focused searching, I was able to learn that Zapier also supports arbitrary code execution jobs, at least as long as code is JavaScript or Python, and follows certain rules. What&amp;#8217;s more, I was able to find &lt;a href="https://github.com/marisadotdev/ZapierLoomlyBluesky"&gt;scripts created&lt;/a&gt; by &lt;a href="https://cromwell-intl.com/open-source/python-social-media-automation/python-program.html"&gt;other people&lt;/a&gt;. After adjusting them a bit, they ran&amp;nbsp;perfectly.&lt;/p&gt;
&lt;p&gt;Integrating Zapier with BlueSky and Mastodon was one of the things I wanted to tackle since I joined a Board, and it makes me really happy that I was able to finally complete&amp;nbsp;this.&lt;/p&gt;
&lt;p&gt;If you are interested in becoming a better tester, advancing the practice of software testing or just want to support my work, consider &lt;a href="http://eepurl.com/tCFsn"&gt;subscribing to &lt;span class="caps"&gt;AST&lt;/span&gt; newsletter&lt;/a&gt; and &lt;a href="https://associationforsoftwaretesting.org/about-the-ast-a-professional-body-for-testers/membership/become-a-member-of-the-the-association-for-software-testing/"&gt;becoming a member&lt;/a&gt;. If you have any questions, ideas or would like to work together, you can email me&amp;nbsp;at &lt;code&gt;mirekdlugosz %at% associationforsoftwaretesting.org&lt;/code&gt; or &lt;a href="https://mirekdlugosz.com/contact.html"&gt;get in touch through other channels&lt;/a&gt;.&lt;/p&gt;</content><category term="Blog"/><category term="AST"/><category term="planet AST"/><category term="planet MoT"/></entry><entry><title>Rust binaries stability</title><link href="https://mirekdlugosz.com/blog/2024/rust-binaries-stability/" rel="alternate"/><published>2024-04-27T22:21:27+02:00</published><updated>2024-04-27T22:21:27+02:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2024-04-27:/blog/2024/rust-binaries-stability/</id><summary type="html">&lt;p&gt;Recently, I&amp;#8217;ve been thinking about Rust stability. Most web search results are discussing stability of the language itself, especially in the context of taking old Rust code and compiling it with recent toolchain. This is obviously important, but not the kind of stability that I had in mind. What …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Recently, I&amp;#8217;ve been thinking about Rust stability. Most web search results are discussing stability of the language itself, especially in the context of taking old Rust code and compiling it with recent toolchain. This is obviously important, but not the kind of stability that I had in mind. What I wanted to know is: &amp;#8220;can I take my old Rust executable and use it on recent&amp;nbsp;Linux?&amp;#8221;&lt;/p&gt;
&lt;p&gt;When I publish this post, Rust toolchain is at version 1.77.2. Rust itself was created in 2006, made publicly available in 2010 and saw first stable release (1.0) in 2015. Obviously I am not interested in anything built before 2015 - you can&amp;#8217;t take bleeding-edge programming language and expect stability. Also, it&amp;#8217;s not like I would be using toolchain that old&amp;nbsp;today.&lt;/p&gt;
&lt;p&gt;Skimming through the documentation, it&amp;#8217;s easy to notice nightly APIs that you have to explicitly opt-in to use. While I heard some of them remain in unchanged form for years, it&amp;#8217;s clear to me that you can&amp;#8217;t expect stability when you use &lt;span class="caps"&gt;API&lt;/span&gt; marked as not&amp;nbsp;stable.&lt;/p&gt;
&lt;p&gt;The typical Rust program will compile to mostly statically linked binary, with &lt;span class="caps"&gt;GNU&lt;/span&gt; libc linked dynamically. This is not the full story, as you can use musl instead, which also supports static linking, and apparently it is possible to avoid any C library if you try really hard. I am not really interested in these cases, especially since they seem to target somewhat special needs, including embedded&amp;nbsp;systems.&lt;/p&gt;
&lt;p&gt;For a typical Rust binary, the long-term stability will depend on two components that are not part of Rust at all: kernel and glibc. Linux takes long-term stability very seriously and is well known for not breaking the user space, so it&amp;#8217;s not really a&amp;nbsp;concern.&lt;/p&gt;
&lt;p&gt;I don&amp;#8217;t know much about &lt;span class="caps"&gt;GNU&lt;/span&gt; libc, but I found &lt;a href="https://developers.redhat.com/blog/2019/08/01/how-the-gnu-c-library-handles-backward-compatibility"&gt;the article discussing techniques employed to ensure backwards compatibility&lt;/a&gt;. This gives me some confidence that they take long-term stability similarly to Linux. When I think about this, about the only case of glibc breaking backwards compatibility I heard of is &lt;a href="https://github.com/ValveSoftware/Proton/issues/6051"&gt;related to anti-cheat software used by games&lt;/a&gt;, and as far as I understand, this was dubious from the very&amp;nbsp;beginning.&lt;/p&gt;
&lt;p&gt;It all indicates that Rust executables should be pretty stable. But as a tester, I know experience beats the theory every time. And I already &lt;em&gt;have&lt;/em&gt; a modern Linux, so if only I could find some old Rust executables, I could see for myself if they still work or&amp;nbsp;not.&lt;/p&gt;
&lt;p&gt;This is easy enough to do with GitHub search. I searched for projects written in Rust that were created before 2018. There&amp;#8217;s nothing special about 2018 - it&amp;#8217;s just a label for &amp;#8220;old&amp;#8221;. Then for every project it found I checked if that&amp;#8217;s an application (and not a library or documentation), if it does releases through GitHub, and what is the oldest release with binary files attached. I repeated that until I decided I have enough. I&amp;#8217;m not trying to be thorough, I want to find an answer in less than an&amp;nbsp;hour.&lt;/p&gt;
&lt;p&gt;I don&amp;#8217;t have any reason to assume these applications are malicious, but since these are random executables from the Internet, I wanted to separate them from the rest of the system. Podman helped me with that. I have&amp;nbsp;used &lt;code&gt;fedora:38&lt;/code&gt; image, only because my host is Fedora 38, and I assumed this way I should minimize potential compatibility problems, if&amp;nbsp;any.&lt;/p&gt;
&lt;p&gt;I don&amp;#8217;t really know how to use these programs, and it was not my goal to learn. I just tried to run each executable&amp;nbsp;with &lt;code&gt;--help&lt;/code&gt; and &lt;code&gt;--version&lt;/code&gt; flags, hoping they are recognized. I expected to see &lt;em&gt;something&lt;/em&gt; if program still works, and get obvious error message if it does&amp;nbsp;not.&lt;/p&gt;
&lt;p&gt;Out of 9 projects tested, 8 appear to still work. The oldest one was from September 2016. The table below shows detailed results. Name of the application is link to the specific version that I&amp;nbsp;tested.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Application&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th style="text-align: center;"&gt;Works&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/BurntSushi/ripgrep/releases/tag/0.0.2"&gt;ripgrep&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;0.0.2&lt;/td&gt;
&lt;td&gt;2016-09-06&lt;/td&gt;
&lt;td style="text-align: center;"&gt;✅ &lt;span class="sr-only"&gt;Yes&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/watchexec/watchexec/releases/tag/1.0.0"&gt;watchexec&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1.0.0&lt;/td&gt;
&lt;td&gt;2016-10-15&lt;/td&gt;
&lt;td style="text-align: center;"&gt;✅&lt;span class="sr-only"&gt;Yes&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/nivekuil/rip/releases/tag/0.8.3"&gt;rip&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;0.8.3&lt;/td&gt;
&lt;td&gt;2016-10-18&lt;/td&gt;
&lt;td style="text-align: center;"&gt;✅&lt;span class="sr-only"&gt;Yes&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/casey/just/releases/tag/v0.2.23"&gt;just&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;v0.2.23&lt;/td&gt;
&lt;td&gt;2016-11-24&lt;/td&gt;
&lt;td style="text-align: center;"&gt;✅&lt;span class="sr-only"&gt;Yes&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/erebe/wstunnel/releases/tag/1.0"&gt;wstunnel&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;2017-12-07&lt;/td&gt;
&lt;td style="text-align: center;"&gt;✅&lt;span class="sr-only"&gt;Yes&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/firecracker-microvm/firecracker/releases/tag/v0.1.0"&gt;Firecracker&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;v.0.1.0&lt;/td&gt;
&lt;td&gt;2018-03-05&lt;/td&gt;
&lt;td style="text-align: center;"&gt;✅ / ❌&lt;span class="sr-only"&gt;It&amp;#8217;s complicated&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/RustScan/RustScan/releases/tag/1.0.1"&gt;RustScan&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1.0.1&lt;/td&gt;
&lt;td&gt;2020-07-19&lt;/td&gt;
&lt;td style="text-align: center;"&gt;✅ / ❌&lt;span class="sr-only"&gt;It&amp;#8217;s complicated&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/mitnk/cicada/releases/tag/v0.9.29"&gt;cicada&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;v0.9.29&lt;/td&gt;
&lt;td&gt;2022-02-24&lt;/td&gt;
&lt;td style="text-align: center;"&gt;✅&lt;span class="sr-only"&gt;Yes&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/swc-project/swc/releases/tag/v1.2.24"&gt;swc&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;v1.2.24&lt;/td&gt;
&lt;td&gt;2020-09-08&lt;/td&gt;
&lt;td style="text-align: center;"&gt;❌&lt;span class="sr-only"&gt;No&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/swc-project/swc/releases/tag/v1.3.105"&gt;swc&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;v1.3.105&lt;/td&gt;
&lt;td&gt;2024-01-21&lt;/td&gt;
&lt;td style="text-align: center;"&gt;✅&lt;span class="sr-only"&gt;Yes&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;I count Firecracker and RustScan as working, but they deserve a bit of explanation. They both showed errors on Fedora, but appear to work on Debian. Errors were &amp;#8220;failed to initialize syslog: InvalidFd&amp;#8221; (Firecracker) and &amp;#8220;./rustscan: cannot execute: required file not found&amp;#8221; (RustScan). For me they sound like missing runtime dependencies, and I consider them fundamentally the same as forgetting to specify a mandatory positional argument. But they also show something I did not consider initially - that long term stability might depend on Linux distribution as&amp;nbsp;well.&lt;/p&gt;
&lt;p&gt;On the other hand, I count swc as the only one that is not working. That&amp;#8217;s because the oldest version, v1.2.24, crashed with a segmentation fault. That was also a case for three later versions I have tried. The first version that appears to work is v1.3.105, released earlier this year. I don&amp;#8217;t know what this application is doing, but it seems to be something&amp;nbsp;unusual.&lt;/p&gt;
&lt;p&gt;To sum up: if you search the web trying to learn if Rust executables are stable in the long term, then the answer seems to be&amp;nbsp;&amp;#8220;yes&amp;#8221;.&lt;/p&gt;</content><category term="Blog"/><category term="Linux"/><category term="Rust"/><category term="planet AST"/><category term="planet MoT"/></entry><entry><title>Why don’t we replace the testers with AI? Postscript</title><link href="https://mirekdlugosz.com/blog/2024/why-dont-we-replace-the-testers-with-ai-postscript/" rel="alternate"/><published>2024-04-16T20:05:21+02:00</published><updated>2024-04-16T20:05:21+02:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2024-04-16:/blog/2024/why-dont-we-replace-the-testers-with-ai-postscript/</id><summary type="html">&lt;p&gt;When researching &lt;a href="https://mirekdlugosz.com/blog/2024/why-dont-we-replace-the-testers-with-ai/"&gt;my previous article&lt;/a&gt;, I stumbled upon &lt;a href="https://davekarpf.substack.com/p/two-failure-modes-of-emerging-technologies"&gt;an essay by Dave Karpf titled &amp;#8220;Two Failure Modes of Emerging Technologies&amp;#8221;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The first failure mode is when the technology is actually successful, finds itself used outside of planned context and that causes a whole swath of unintended consequences. Second - technology fails …&lt;/p&gt;</summary><content type="html">&lt;p&gt;When researching &lt;a href="https://mirekdlugosz.com/blog/2024/why-dont-we-replace-the-testers-with-ai/"&gt;my previous article&lt;/a&gt;, I stumbled upon &lt;a href="https://davekarpf.substack.com/p/two-failure-modes-of-emerging-technologies"&gt;an essay by Dave Karpf titled &amp;#8220;Two Failure Modes of Emerging Technologies&amp;#8221;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The first failure mode is when the technology is actually successful, finds itself used outside of planned context and that causes a whole swath of unintended consequences. Second - technology fails to deliver what it promised, but &lt;em&gt;still&lt;/em&gt; finds itself widely used and incorporated into critical systems. Everyone learns to works around problems, or worse - stops to see there are problems to begin&amp;nbsp;with.&lt;/p&gt;
&lt;p&gt;Most of &lt;span class="caps"&gt;AI&lt;/span&gt; critique falls closer to the first mode of failure. My article is no different - my main argument is that &lt;span class="caps"&gt;AI&lt;/span&gt; will not get as good as it needs to be to replace testers anytime&amp;nbsp;soon.&lt;/p&gt;
&lt;p&gt;But if I were taking bets, my money would be on &lt;span class="caps"&gt;AI&lt;/span&gt; following the second mode of failure - it will not live up to it&amp;#8217;s hype, and still get widely&amp;nbsp;deployed.&lt;/p&gt;
&lt;p&gt;It&amp;#8217;s unfortunate I did not spend more time reflecting on&amp;nbsp;this.&lt;/p&gt;</content><category term="Blog"/><category term="testing"/></entry><entry><title>Why don’t we replace the testers with AI?</title><link href="https://mirekdlugosz.com/blog/2024/why-dont-we-replace-the-testers-with-ai/" rel="alternate"/><published>2024-04-16T20:04:12+02:00</published><updated>2024-04-16T20:04:12+02:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2024-04-16:/blog/2024/why-dont-we-replace-the-testers-with-ai/</id><summary type="html">&lt;p&gt;The Association for Software Testing is crowd-sourcing a book, &lt;a href="https://associationforsoftwaretesting.org/navigating-the-world-as-a-context-driven-tester-book/"&gt;&lt;em&gt;Navigating the World as a Context-Driven Tester&lt;/em&gt;&lt;/a&gt;. The book is edited by &lt;a href="https://therockertester.wordpress.com/"&gt;Lee Hawkins&lt;/a&gt;, who posts questions on &lt;a href="https://sw-development-is.social/web/@AST"&gt;Mastodon&lt;/a&gt;, &lt;a href="https://www.linkedin.com/company/association-for-software-testing/"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://bsky.app/profile/ast-news.bsky.social"&gt;BlueSky&lt;/a&gt;, &lt;a href="https://twitter.com/AST_News"&gt;Twitter&lt;/a&gt;, &lt;a href="https://associationforsoftwaretesting.org/2016/11/13/ast-members-slack/"&gt;Slack&lt;/a&gt;, and &lt;a href="http://eepurl.com/tCFsn"&gt;the &lt;span class="caps"&gt;AST&lt;/span&gt; mailing list&lt;/a&gt;. You don&amp;#8217;t have to be a member to give your answer to …&lt;/p&gt;</summary><content type="html">&lt;p&gt;The Association for Software Testing is crowd-sourcing a book, &lt;a href="https://associationforsoftwaretesting.org/navigating-the-world-as-a-context-driven-tester-book/"&gt;&lt;em&gt;Navigating the World as a Context-Driven Tester&lt;/em&gt;&lt;/a&gt;. The book is edited by &lt;a href="https://therockertester.wordpress.com/"&gt;Lee Hawkins&lt;/a&gt;, who posts questions on &lt;a href="https://sw-development-is.social/web/@AST"&gt;Mastodon&lt;/a&gt;, &lt;a href="https://www.linkedin.com/company/association-for-software-testing/"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://bsky.app/profile/ast-news.bsky.social"&gt;BlueSky&lt;/a&gt;, &lt;a href="https://twitter.com/AST_News"&gt;Twitter&lt;/a&gt;, &lt;a href="https://associationforsoftwaretesting.org/2016/11/13/ast-members-slack/"&gt;Slack&lt;/a&gt;, and &lt;a href="http://eepurl.com/tCFsn"&gt;the &lt;span class="caps"&gt;AST&lt;/span&gt; mailing list&lt;/a&gt;. You don&amp;#8217;t have to be a member to give your answer to the most recent prompt, &amp;#8220;Why don’t we replace the testers with &lt;span class="caps"&gt;AI&lt;/span&gt;?&amp;#8221;. Mine is&amp;nbsp;below.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;We can&amp;#8217;t replace testers with &lt;span class="caps"&gt;AI&lt;/span&gt; because, as of April 2024, there&amp;#8217;s no &lt;span class="caps"&gt;AI&lt;/span&gt; capable of doing all the things that testers are&amp;nbsp;doing.&lt;/p&gt;
&lt;p&gt;Now, that&amp;#8217;s the answer that misses the point of the question. Obviously, the intent is: assuming &lt;span class="caps"&gt;AI&lt;/span&gt; develops rapidly and may get to the point where it is capable of performing some / most / all of the things that testers do, are there any reasons to not replace testers with &lt;span class="caps"&gt;AI&lt;/span&gt;?&lt;/p&gt;
&lt;p&gt;That &amp;#8220;assuming&amp;#8221; is doing some heavy lifting here. Unfortunately, &lt;span class="caps"&gt;AI&lt;/span&gt; discourse is ridden with hype, daydreamers and straight-up con artists. I think it&amp;#8217;s important to try to tip the scales by highlighting the problems that need to be solved. Let&amp;#8217;s dive into what exactly &amp;#8220;assuming&amp;#8221; here&amp;nbsp;means.&lt;/p&gt;
&lt;p&gt;We assume that &lt;span class="caps"&gt;AI&lt;/span&gt; of the future will not hallucinate. &amp;#8220;Hallucination&amp;#8221; in the context of &lt;span class="caps"&gt;AI&lt;/span&gt; is a marketing term used for situations where &lt;span class="caps"&gt;AI&lt;/span&gt; system outputs statements that are not true. This is one of the most widely known problems, as it takes only a few minutes of interacting with &lt;span class="caps"&gt;AI&lt;/span&gt; bot for it to say things that are demonstrably false. This is also a heavily researched topic. There is &lt;a href="https://arxiv.org/abs/2401.11817"&gt;some research showing that &amp;#8220;hallucinations&amp;#8221; might be inevitable in the current breed of AIs&lt;/a&gt;. For all we know, no company has been able to solve that yet - OpenAI, Microsoft, Google and everyone else just &lt;a href="https://matt.si/2024-02/llms-overpromised/"&gt;add an increasing number of layers around their models&lt;/a&gt;, hoping that users encounter canned response earlier than a problematic&amp;nbsp;one.&lt;/p&gt;
&lt;p&gt;We assume that we have not yet reached the limit of what current &lt;span class="caps"&gt;AI&lt;/span&gt; technology can do. OpenAI takes a position that all the problems in &lt;span class="caps"&gt;AI&lt;/span&gt; space right now are resource-bound. What they say is that &lt;a href="https://davekarpf.substack.com/p/lets-tap-the-brakes-on-this-ai-hype"&gt;we just need a little larger models and little faster computers&lt;/a&gt;; what they imply is that we already have all the theory needed to build any &lt;span class="caps"&gt;AI&lt;/span&gt; we might need. If you go to any computer science or psychology department in the world, I&amp;#8217;m not sure if you will find a single person that shares that&amp;nbsp;belief.&lt;/p&gt;
&lt;p&gt;We assume that if there are any problems that are fundamentally qualitative in their nature (i.e. won&amp;#8217;t be solved by a little faster computer), we will solve them. The tricky thing about unknown unknowns is that you don&amp;#8217;t know about them. And the thing about breakthroughs is that you don&amp;#8217;t know when they are going to happen, if at all. &lt;a href="https://en.wikipedia.org/wiki/Hilbert%27s_problems"&gt;Hilbert&amp;#8217;s problems&lt;/a&gt; are a good exemplification - some of the problems are still unresolved after decades of work, while others were solved soon after their posting. When Hilbert announced them, it was impossible to predict which are going to be&amp;nbsp;which.&lt;/p&gt;
&lt;p&gt;We assume there are sustainable business models that can support &lt;span class="caps"&gt;AI&lt;/span&gt;. &lt;a href="https://www.wsj.com/tech/ai/ais-costly-buildup-could-make-early-products-a-hard-sell-bdd29b9f"&gt;All current &lt;span class="caps"&gt;AI&lt;/span&gt; products are developed and run at the loss&lt;/a&gt;. This is not a necessarily a bad thing - every research takes resources before it turns into something that can generate profit, and most of companies have to lose money before they can earn enough to sustain themselves (this is what &amp;#8220;investment&amp;#8221; means). However, right now &lt;a href="https://medium.com/@colin.fraser/generative-ai-is-a-hammer-and-no-one-knows-what-is-and-isnt-a-nail-4c7f3f0911aa"&gt;we still don&amp;#8217;t know what kind of problems &lt;span class="caps"&gt;AI&lt;/span&gt; is going to be good at solving&lt;/a&gt;, and &lt;a href="https://www.wheresyoured.at/peakai/"&gt;many of &lt;span class="caps"&gt;AI&lt;/span&gt; leaders seem to be fundamentally disinterested in engaging with this question&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We assume that &lt;span class="caps"&gt;AI&lt;/span&gt; natural resource usage can be lowered and kept at manageable level. &lt;a href="https://www.technologyreview.com/2022/11/14/1063192/were-getting-a-better-idea-of-ais-true-carbon-footprint/"&gt;&lt;span class="caps"&gt;AI&lt;/span&gt; models are very resource-intensive, especially during the training&lt;/a&gt;. &lt;a href="https://www.forbes.com/sites/davidjeans/2021/08/19/data-in-the-dark-how-big-tech-secretly-secured-800-million-in-tax-breaks-for-data-centers/"&gt;Data centers are well-known sponges for electricity and water&lt;/a&gt;. As I&amp;#8217;m writing these words, we are in the &lt;a href="https://climate.copernicus.eu/march-2024-10th-consecutive-record-warm-month-globally"&gt;streak of 10 record warm months&lt;/a&gt;, and judging by the first half of April, it&amp;#8217;s going to be at least 11 record warm months. The climate change is the biggest challenge that humanity faces right now. The nations are &lt;em&gt;finally&lt;/em&gt; starting to create and enforce laws that attempt to limit the natural resources usage, many companies pledge to achieve net-zero emissions within a next decade (including Microsoft and Google), and &lt;a href="https://www.mckinsey.com/industries/consumer-packaged-goods/our-insights/consumers-care-about-sustainability-and-back-it-up-with-their-wallets"&gt;customers are actually taking environmental costs into consideration in their purchasing decisions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We assume that &lt;span class="caps"&gt;AI&lt;/span&gt; can be trained on legal material, and &lt;span class="caps"&gt;AI&lt;/span&gt; output will not create additional legal risks for users. Creative workers were first to be hit by &lt;span class="caps"&gt;AI&lt;/span&gt; work, and &lt;a href="https://www.theverge.com/2023/1/16/23557098/generative-ai-art-copyright-legal-lawsuit-stable-diffusion-midjourney-deviantart"&gt;many were not happy to learn that these systems are trained on their pieces without explicit consent&lt;/a&gt;. &lt;a href="https://www.investopedia.com/new-york-times-sues-openai-over-chatgpt-s-alleged-copyright-infringement-8420351"&gt;New York Times is in legal battle against OpenAI on the ground of copyright infringement&lt;/a&gt;. This, and many more lawsuits in the future will establish the limits, legal rules and total costs of &lt;span class="caps"&gt;AI&lt;/span&gt; usage and training. Smarter companies are holding off wide deployments of &lt;span class="caps"&gt;AI&lt;/span&gt; tools until someone else fights these legal battles and establishes precedent. Actual creators of these tools count themselves among smarter&amp;nbsp;companies.&lt;/p&gt;
&lt;p&gt;We assume that general quality of the work &lt;span class="caps"&gt;AI&lt;/span&gt; tools produce is going to be acceptable. Every time I see anyone pitching an &lt;span class="caps"&gt;AI&lt;/span&gt; tool, I have a single question - if the tool is so great, &lt;a href="https://www.joelonsoftware.com/2001/05/05/what-is-the-work-of-dogs-in-this-country/"&gt;why don&amp;#8217;t you use it yourself&lt;/a&gt;? You saw Microsoft talking a lot about purported time-savings coming from using GitHub Copilot, but you never saw Microsoft talking about using Copilot for Windows, Office or Visual Studio development. Makes you wonder&amp;nbsp;why.&lt;/p&gt;
&lt;p&gt;All of above make me think that &amp;#8220;&lt;span class="caps"&gt;AI&lt;/span&gt; taking testers jobs&amp;#8221; might not be a problem that we are going to have anytime&amp;nbsp;soon.&lt;/p&gt;</content><category term="Blog"/><category term="AST"/><category term="planet AST"/><category term="planet MoT"/><category term="testing"/></entry><entry><title>30 Days of AI in Testing experience report</title><link href="https://mirekdlugosz.com/blog/2024/30-days-of-ai-in-testing-experience-report/" rel="alternate"/><published>2024-04-02T21:49:26+02:00</published><updated>2024-04-02T21:49:26+02:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2024-04-02:/blog/2024/30-days-of-ai-in-testing-experience-report/</id><summary type="html">&lt;p&gt;In March, Ministry of Testing held 30 Days of &lt;span class="caps"&gt;AI&lt;/span&gt; in Testing challenge. I participated, and post my general impressions&amp;nbsp;below.&lt;/p&gt;
&lt;p&gt;If you have never heard of &amp;#8220;$x days of $something&amp;#8221; challenges, the idea is simple - there is some kind of prompt posted every day, and your goal is to …&lt;/p&gt;</summary><content type="html">&lt;p&gt;In March, Ministry of Testing held 30 Days of &lt;span class="caps"&gt;AI&lt;/span&gt; in Testing challenge. I participated, and post my general impressions&amp;nbsp;below.&lt;/p&gt;
&lt;p&gt;If you have never heard of &amp;#8220;$x days of $something&amp;#8221; challenges, the idea is simple - there is some kind of prompt posted every day, and your goal is to write a short piece in response. You publish the work on a blog, dedicated forum or on social media. Prompts might include a task to complete, and these tasks will vary from very simple to somewhat complex. Some days majority of your time will be spent on the task itself, not&amp;nbsp;writing.&lt;/p&gt;
&lt;p&gt;I have heard about challenges like that years ago, but this was my first time attempting&amp;nbsp;it.&lt;/p&gt;
&lt;p&gt;One thing that I was definitely not prepared for is the pace. To be honest, it was absolutely exhausting. I started on a high note, fully motivated to write something every single day. At 9th day there was a technical glitch that caused a daily prompt to be published much later than usual, and I skipped that day due to scheduling problems. Only then I realized how badly I needed the break, and that 30 days straight are not going to be sustainable for me. I ended up skipping 5&amp;nbsp;days.&lt;/p&gt;
&lt;p&gt;Finding the right rhythm and schedule was somewhat challenging, too. Prompts were published at the forum at 1 &lt;span class="caps"&gt;AM&lt;/span&gt; my local time, and arrived by email at around 11 &lt;span class="caps"&gt;AM&lt;/span&gt;. I usually checked the forum before going to sleep, to give myself some time to think about the reply. Then I would try to kick off the day by writing a reply as one of the first things in the morning. I found it much harder to fit writing in my afternoon / evening schedule. On the other hand, composing a reply could easily get out of control, creeping into my working hours. The solution I applied is not revolutionary - after few days I started to timebox myself much more aggressively. This resulted in omitting some points I thought about raising, but couldn&amp;#8217;t carry out in satisfactory manner in reasonable&amp;nbsp;time.&lt;/p&gt;
&lt;p&gt;I participated primarily to see how others are using &lt;span class="caps"&gt;AI&lt;/span&gt; in their day-to-day work, or hope to use it in the future. The challenge definitely did push me into taking a closer look at various tools and getting some hands-on experience with them. I was able to try them in tasks that I wouldn&amp;#8217;t come up with on my own. That gave me some idea what these tools might be good at, and where they definitely need some more&amp;nbsp;work.&lt;/p&gt;
&lt;p&gt;At the same time, I wouldn&amp;#8217;t say it changed my overall approach to &lt;span class="caps"&gt;AI&lt;/span&gt;. I haven&amp;#8217;t seen anything that would change my mind about the problems pullulating in the entire space, including output quality, copyright, privacy, data security and natural resource&amp;nbsp;usage.&lt;/p&gt;
&lt;p&gt;Few of the tasks involved using chatbots to handle some generic testing tasks - &lt;a href="https://club.ministryoftesting.com/t/day-10-critically-analyse-ai-generated-tests/74987"&gt;coming up with test ideas&lt;/a&gt;, &lt;a href="https://club.ministryoftesting.com/t/day-11-generate-test-data-using-ai-and-evaluate-its-efficacy/75083"&gt;generating test data&lt;/a&gt;, &lt;a href="https://club.ministryoftesting.com/t/day-14-generate-ai-test-code-and-share-your-experience/75133"&gt;creating test code&lt;/a&gt; or &lt;a href="https://club.ministryoftesting.com/t/day-24-investigate-code-explanation-techniques-and-share-your-insights/75364"&gt;explaining existing code&lt;/a&gt;. I have used Microsoft Bing Copilot for these. Every time I made a point to give it a problem that I have already faced in the past, so I can compare the output with some baseline reference material. There were some surprises, when Copilot did things I didn&amp;#8217;t think about myself. But almost without a fail, Copilot would also omit something that I did cover. For me, that tips the balance against using LLMs for most of the tasks - I might be slower, but I produce higher quality work, gain experience and acquire some new domain-specific knowledge. These are things that are going to pay off manifold in the future. My current opinion is that by using &lt;span class="caps"&gt;AI&lt;/span&gt; systems you might be faster, but you miss opportunities to learn and upskill&amp;nbsp;yourself.&lt;/p&gt;
&lt;p&gt;One of the reasons to take part was to build connections and interact with other members. I don&amp;#8217;t think it worked for me. Doing a task and writing a response was challenging and time consuming already, I just wasn&amp;#8217;t able to put conversations with others on top of that. These kinds of interactions were not facilitated by the forum software, which displays a long list of messages one under another. It was especially problematic in the early days where each thread saw hundreds of responses. To be fair, I did saw more people responding to other folks later on, when activity has toned down a&amp;nbsp;bit.&lt;/p&gt;
&lt;p&gt;Unexpected benefit of taking part in the challenge was that it forced me to write. A lot. In total, I have written a bit over 8500 words (51 000 characters). For comparison, all the blog posts published so far on this website total to 42 500 words and a bit over 280 000 characters. To put things in perspective, in these 30 days I have written about 20% of what I&amp;#8217;ve published here in 8 and a half &lt;em&gt;years&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Especially in early days, the challenge kind of unlocked something in me, and I became much more prolific writer than ever before. Apart from forum posts, I published two articles on this blog, wrote notes, multiple emails and became much more active on LinkedIn or Mastodon. I even have two blog posts written that are not published yet - something that I haven&amp;#8217;t experienced since 2012 or so. My only worry is this change might not be permanent and will fade as quickly as it appeared; that after short period of high productivity I will succumb to levels even lower than&amp;nbsp;before.&lt;/p&gt;
&lt;p&gt;The final observation I&amp;#8217;m going to share is that Ministry of Testing forums were rather unusable in the first half of the month. While the goal of challenge is to do a small task every day, it&amp;#8217;s only natural that not everyone can do that. It&amp;#8217;s encouraged to write a reply to previous prompt at later date - better to do things late than to drop out. But as a result, all threads from recent days constantly received new replies. The default view of the forum, where threads are listed by time of last reply, was constantly swamped by challenge threads. Things improved after two or three weeks, when initial excitement faded and only most persistent people remained. Most of them responded daily, so previous days saw little activity and unrelated threads were easier to&amp;nbsp;notice.&lt;/p&gt;
&lt;p&gt;Honestly, I don&amp;#8217;t know if I will ever participate in similar challenge in the future. I think it will require a topic that is highly interesting for me. If I do participate, I will definitely not go in with the goal of writing something every single day. I imagine I will try to respond to third, maybe half of all prompts. I will also give myself permission to slack and skip days from the get-go. I will definitely timebox my daily participation aggressively from the very first day. Perhaps I will focus exclusively on hands-on experience and completing the tasks, while skipping the writing part. Another approach I might try is treating that more as a networking event, focus on reading what others are publishing, ask clarifying or supplementary questions and encourage them to keep&amp;nbsp;going.&lt;/p&gt;
&lt;p&gt;I thought I&amp;#8217;ll finish with some numbers. I have participated in 25 days, which means I have skipped 5 days. In total, I have written 51 562 characters, which gives a bit over 2000 characters per post. For reference, average article on this blog is about 6000 characters, and this article is about 7700&amp;nbsp;characters.&lt;/p&gt;
&lt;p&gt;The chart below shows the number of replies to each day prompt, as of April 1st. Since one person could write multiple posts in each thread, this is at best a proxy variable for much more interesting survival analysis. Nonetheless, the pattern is pretty&amp;nbsp;clear.&lt;/p&gt;
&lt;figure&gt;
        &lt;a href="https://mirekdlugosz.com/blog/2024/30-days-of-ai-in-testing-experience-report/30-days-of-ai-in-testing-experience-report/responses-plot.png"&gt;
            &lt;img src="https://mirekdlugosz.com/blog/2024/30-days-of-ai-in-testing-experience-report/30-days-of-ai-in-testing-experience-report/responses-plot-min.png" title="Number of responses in each day thread" alt="Number of responses in each day thread" loading="lazy"&gt;
        &lt;/a&gt;
        
    &lt;/figure&gt;</content><category term="Blog"/><category term="planet AST"/><category term="planet MoT"/><category term="testing"/></entry><entry><title>Don’t blindly serve WebP format</title><link href="https://mirekdlugosz.com/blog/2024/dont-blindly-serve-webp-format/" rel="alternate"/><published>2024-03-12T17:57:46+01:00</published><updated>2024-03-12T17:57:46+01:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2024-03-12:/blog/2024/dont-blindly-serve-webp-format/</id><summary type="html">&lt;p&gt;If you have done any webdev work in last few years, you must have heard about WebP. It&amp;#8217;s an image format that promises up to 34% smaller file sizes without noticeable quality downgrade. It&amp;#8217;s pretty much universally supported since late&amp;nbsp;2020.&lt;/p&gt;
&lt;p&gt;With smaller file sizes and widespread support …&lt;/p&gt;</summary><content type="html">&lt;p&gt;If you have done any webdev work in last few years, you must have heard about WebP. It&amp;#8217;s an image format that promises up to 34% smaller file sizes without noticeable quality downgrade. It&amp;#8217;s pretty much universally supported since late&amp;nbsp;2020.&lt;/p&gt;
&lt;p&gt;With smaller file sizes and widespread support, you might think it&amp;#8217;s a good idea to just serve all your images in WebP. Or, if you want to be extra backward-compatible - serve WebP to all browsers that claim to support it, and original image to remaining&amp;nbsp;few.&lt;/p&gt;
&lt;p&gt;I also thought it&amp;#8217;s a good idea, and made a switch on this very website. I&amp;#8217;m creating WebP with a compression factor set to 80. And then I noticed that one file is actually larger after&amp;nbsp;conversion.&lt;/p&gt;
&lt;p&gt;Following this thread, I compared size of all WebP images with their original counterparts. Turned out WebP produced larger files in 3% (4 out of 117). In worst case, 20 kB &lt;span class="caps"&gt;PNG&lt;/span&gt; file turned into 122 kB WebP - over sixfold increase in&amp;nbsp;size!&lt;/p&gt;
&lt;p&gt;Since then, when I generate WebP, I compare file size to original and keep it only if new format produces smaller file. This way browsers will always receive the smallest file I can produce, regardless of the&amp;nbsp;format.&lt;/p&gt;
&lt;p&gt;I guess the main takeaway here is ages old &amp;#8220;measure before&amp;nbsp;optimizing&amp;#8221;.&lt;/p&gt;</content><category term="Blog"/><category term="planet AST"/><category term="planet MoT"/><category term="testing"/><category term="tutorial"/></entry><entry><title>What’s the best format for a test plan?</title><link href="https://mirekdlugosz.com/blog/2024/whats-the-best-format-for-a-test-plan/" rel="alternate"/><published>2024-03-04T10:15:57+01:00</published><updated>2024-03-04T10:15:57+01:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2024-03-04:/blog/2024/whats-the-best-format-for-a-test-plan/</id><summary type="html">&lt;p&gt;The Association for Software Testing is crowd-sourcing a book, &lt;a href="https://associationforsoftwaretesting.org/navigating-the-world-as-a-context-driven-tester-book/"&gt;&lt;em&gt;Navigating the World as a Context-Driven Tester&lt;/em&gt;&lt;/a&gt;. The book is edited by &lt;a href="https://therockertester.wordpress.com/"&gt;Lee Hawkins&lt;/a&gt;, who posts questions on &lt;a href="https://twitter.com/AST_News"&gt;Twitter&lt;/a&gt;, &lt;a href="https://www.linkedin.com/company/association-for-software-testing/"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://sw-development-is.social/web/@AST"&gt;Mastodon&lt;/a&gt;, &lt;a href="https://associationforsoftwaretesting.org/2016/11/13/ast-members-slack/"&gt;Slack&lt;/a&gt;, and &lt;a href="http://eepurl.com/tCFsn"&gt;the &lt;span class="caps"&gt;AST&lt;/span&gt; mailing list&lt;/a&gt;. You don&amp;#8217;t have to be a member to give your answer to the …&lt;/p&gt;</summary><content type="html">&lt;p&gt;The Association for Software Testing is crowd-sourcing a book, &lt;a href="https://associationforsoftwaretesting.org/navigating-the-world-as-a-context-driven-tester-book/"&gt;&lt;em&gt;Navigating the World as a Context-Driven Tester&lt;/em&gt;&lt;/a&gt;. The book is edited by &lt;a href="https://therockertester.wordpress.com/"&gt;Lee Hawkins&lt;/a&gt;, who posts questions on &lt;a href="https://twitter.com/AST_News"&gt;Twitter&lt;/a&gt;, &lt;a href="https://www.linkedin.com/company/association-for-software-testing/"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://sw-development-is.social/web/@AST"&gt;Mastodon&lt;/a&gt;, &lt;a href="https://associationforsoftwaretesting.org/2016/11/13/ast-members-slack/"&gt;Slack&lt;/a&gt;, and &lt;a href="http://eepurl.com/tCFsn"&gt;the &lt;span class="caps"&gt;AST&lt;/span&gt; mailing list&lt;/a&gt;. You don&amp;#8217;t have to be a member to give your answer to the most recent prompt, &amp;#8220;What&amp;#8217;s the best format for a test plan?&amp;#8221;. Mine is&amp;nbsp;below.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;It should come as no surprise that there is no single &amp;#8220;best&amp;#8221;&amp;nbsp;format.&lt;/p&gt;
&lt;p&gt;Each document that we create should be &lt;em&gt;for someone&lt;/em&gt; and should respond to some real or perceived &lt;em&gt;need&lt;/em&gt;. Otherwise, why bother writing&amp;nbsp;it?&lt;/p&gt;
&lt;p&gt;I imagine perspective on planning changes as you move up in corporate hierarchy. For directors and executives it&amp;#8217;s all about expected resource utilization and budgeting. I think they want to know how many people to hire, how many devices they need to buy, how much their cloud costs are going to go up; and what they can skip or cut out while still meeting the strategic objectives. I wouldn&amp;#8217;t know, I have never had this kind of&amp;nbsp;job.&lt;/p&gt;
&lt;p&gt;As an individual contributor, my perspective is different. When I am asked to create a test plan, my first thought is &amp;#8220;who am I writing this for?&amp;#8221; &lt;em&gt;I&lt;/em&gt; might not need it, and I &lt;em&gt;definitely&lt;/em&gt; don&amp;#8217;t need it in this very heavy form exemplified by templates floating around the web. Other testers in my team often don&amp;#8217;t need it either - they are just glad they didn&amp;#8217;t have to write it. Developers in my team don&amp;#8217;t care. Other testers in adjacent teams are usually busy with their own things. My manager might be happy to see it, and he might or might not forward it up the chain, but I never heard back from as close as skip-level&amp;nbsp;manager.&lt;/p&gt;
&lt;p&gt;Usually after test plan is written, there&amp;#8217;s a request for review and comments sent to a number of people. In the best case I will get few questions pointing out parts that could be clarified, or some suggestions for things that I have missed. I appreciate them. This helps me to be more thorough and allows others to better understand what is happening in other parts of the&amp;nbsp;project.&lt;/p&gt;
&lt;p&gt;However, I doubt if test plan is the most effective way of achieving these goals. While everything that test plans usually cover &lt;em&gt;is&lt;/em&gt; important, I feel whatever I put in document should be the conclusion of conversation, not the beginning of&amp;nbsp;it.&lt;/p&gt;
&lt;p&gt;Let&amp;#8217;s say I think we will need a team of three people to complete all testing activities in given time, while there&amp;#8217;s only me and one colleague. Putting that somewhere deep in test plan will not make a new person appear out from nowhere. In fact, I am much better off talking with my manager about this - because either we need to arrange for new person right now, or we need to highlight to other stakeholders that certain things are at risk because there are not enough&amp;nbsp;people.&lt;/p&gt;
&lt;p&gt;The same applies to any hardware and software that might be needed for testing. Most companies have processes for obtaining these, which I would have to follow independently of any test plan document. If I have any doubts on whether my request is going to be accepted, I&amp;#8217;d better talk with my manager instead of hoping he will notice this crucial detail somewhere in a test plan that I send&amp;nbsp;him.&lt;/p&gt;
&lt;p&gt;This is also true for test ideas that I might put in test plan. I am happy to share them with developers - the best thing that can happen is that I bring their attention to a thing they have not thought about, and they will fix the bugs before any code is subjected to testing. But I haven&amp;#8217;t seen any evidence that a long, mostly irrelevant document is the best way to start that&amp;nbsp;conversation.&lt;/p&gt;
&lt;p&gt;In fact, I consider thinking about test ideas to be the most valuable part of test planning. Usually I would try to create a list of things I think would be worth testing, based on any resources I have available at the time. Often while writing things down I will realize I&amp;#8217;m not exactly sure what is supposed to happen - which means that either I didn&amp;#8217;t fully understood what&amp;#8217;s going on, or I found a problem with specification. Sometimes I will also write down questions I will want to find answers&amp;nbsp;for.&lt;/p&gt;
&lt;p&gt;That list of test ideas is going to be my starting point for actual testing. However, not all test ideas need to be performed repeatedly, and not all of them are worth automating - which leaves open the question if they should be included in any kind of formal test&amp;nbsp;plan.&lt;/p&gt;
&lt;p&gt;And even if I did include the whole list in a test plan, considering it to be complete would be a grave mistake. I assume I will add more items later. Some ideas might come to me while doing other things, some might be prompted by conversations that are going to happen as project evolves, and some I will create in response to the actual behavior of developed software. When I test, there are always things that pique my interest, and more often than not they are worth following - no matter if they are included in test plan or&amp;nbsp;not.&lt;/p&gt;
&lt;p&gt;I think all the above points, and then some, are now widely understood in the industry. That&amp;#8217;s why test plans are not so ubiquitous as they once were. Personally, I can count on one hand the number of times I&amp;#8217;ve been asked to create a test plan in a last few years. The software I work on is in continuous development and sees many relatively small releases. Large things, if they happen, take months to develop, not years. We practice backlog refinements, three amigos meetings and collectively write acceptance criteria, which often &lt;em&gt;are&lt;/em&gt; a lightweight form of test plan. Along with the other testers we are in the same team as developers, closely watch progress on new features and get a chance to interact with early versions of the software. I participate in code reviews and suggest improvements to unit tests, which allows me to share test&amp;nbsp;ideas.&lt;/p&gt;
&lt;p&gt;We get many of the same benefits that test plan could provide, but in less formal way and without spending much time writing it.
However, if I were asked to write a test plan, I would start by reading &lt;a href="https://developsense.com/blog/2008/12/what-should-test-plan-contain"&gt;&amp;#8220;What Should A Test Plan Contain?&amp;#8221; by Michael Bolton&lt;/a&gt;.&lt;/p&gt;</content><category term="Blog"/><category term="AST"/><category term="planet AST"/><category term="planet MoT"/><category term="testing"/></entry><entry><title>Association for Software Testing Board of Directors report - February 2024</title><link href="https://mirekdlugosz.com/blog/2024/ast-bod-report-february-2024/" rel="alternate"/><published>2024-03-01T21:38:26+01:00</published><updated>2024-03-01T21:38:26+01:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2024-03-01:/blog/2024/ast-bod-report-february-2024/</id><summary type="html">&lt;p&gt;In my &lt;a href="https://associationforsoftwaretesting.org/about-the-ast-a-professional-body-for-testers/board-of-directors/election-proceedings/2023-election/2023-election-mirek-dlugosz/"&gt;Association for Software Testing 2023 elections proceedings&lt;/a&gt; I advocated for transparency. In the spirit of practising what I preach, this blog post summarizes my work for &lt;span class="caps"&gt;AST&lt;/span&gt; in February&amp;nbsp;2024.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fixed a problem where email sent to Group is not distributed to all members of that Group. Google …&lt;/li&gt;&lt;/ul&gt;</summary><content type="html">&lt;p&gt;In my &lt;a href="https://associationforsoftwaretesting.org/about-the-ast-a-professional-body-for-testers/board-of-directors/election-proceedings/2023-election/2023-election-mirek-dlugosz/"&gt;Association for Software Testing 2023 elections proceedings&lt;/a&gt; I advocated for transparency. In the spirit of practising what I preach, this blog post summarizes my work for &lt;span class="caps"&gt;AST&lt;/span&gt; in February&amp;nbsp;2024.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fixed a problem where email sent to Group is not distributed to all members of that Group. Google support was not particularly helpful, although I must note that there was actual human involved, and she did try. A fix involved changing the way our Wordpress site is sending emails. Documented everything&amp;nbsp;internally.&lt;/li&gt;
&lt;li&gt;Pushed Phone.com migration (see &lt;a href="https://mirekdlugosz.com/blog/2024/ast-bod-report-january-2024/"&gt;previous report&lt;/a&gt;). Wrote down all cost saving opportunities and action items, assigned these to other Board members. Not all items are resolved&amp;nbsp;yet.&lt;/li&gt;
&lt;li&gt;Fixed a &lt;a href="https://associationforsoftwaretesting.org/bbst-black-box-software-testing-courses/graduates/"&gt;&lt;span class="caps"&gt;BBST&lt;/span&gt; Graduates list page&lt;/a&gt;. There were some strange things in &lt;span class="caps"&gt;HTML&lt;/span&gt; that caused Wordpress to parse it badly, and in turn it displayed all&amp;nbsp;wrong.&lt;/li&gt;
&lt;li&gt;Fixed a problem of periodic posts on LinkedIn displaying black box instead of YouTube video&amp;nbsp;preview.&lt;/li&gt;
&lt;li&gt;Updated internal dictionary / documentation of shared email&amp;nbsp;accounts&lt;/li&gt;
&lt;li&gt;Monthly Board of Directors&amp;nbsp;meeting.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you are interested in becoming a better tester, advancing the practice of software testing or just want to support my work, consider &lt;a href="http://eepurl.com/tCFsn"&gt;subscribing to &lt;span class="caps"&gt;AST&lt;/span&gt; newsletter&lt;/a&gt; and &lt;a href="https://associationforsoftwaretesting.org/about-the-ast-a-professional-body-for-testers/membership/become-a-member-of-the-the-association-for-software-testing/"&gt;becoming a member&lt;/a&gt;. If you have any questions, ideas or would like to work together, you can email me&amp;nbsp;at &lt;code&gt;mirekdlugosz %at% associationforsoftwaretesting.org&lt;/code&gt; or &lt;a href="https://mirekdlugosz.com/contact.html"&gt;get in touch through other channels&lt;/a&gt;.&lt;/p&gt;</content><category term="Blog"/><category term="AST"/><category term="planet AST"/><category term="planet MoT"/></entry><entry><title>Association for Software Testing Board of Directors report - January 2024</title><link href="https://mirekdlugosz.com/blog/2024/ast-bod-report-january-2024/" rel="alternate"/><published>2024-02-01T22:08:38+01:00</published><updated>2024-02-01T22:08:38+01:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2024-02-01:/blog/2024/ast-bod-report-january-2024/</id><summary type="html">&lt;p&gt;In my &lt;a href="https://associationforsoftwaretesting.org/about-the-ast-a-professional-body-for-testers/board-of-directors/election-proceedings/2023-election/2023-election-mirek-dlugosz/"&gt;Association for Software Testing 2023 elections proceedings&lt;/a&gt; I advocated for transparency. In the spirit of practising what I preach, this blog post summarizes my work for &lt;span class="caps"&gt;AST&lt;/span&gt; in January&amp;nbsp;2024.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Implemented &lt;span class="caps"&gt;DNS&lt;/span&gt; changes required for VeryConnect (software running our members&amp;nbsp;portal)&lt;/li&gt;
&lt;li&gt;Did about 75% of Phone.com migration …&lt;/li&gt;&lt;/ul&gt;</summary><content type="html">&lt;p&gt;In my &lt;a href="https://associationforsoftwaretesting.org/about-the-ast-a-professional-body-for-testers/board-of-directors/election-proceedings/2023-election/2023-election-mirek-dlugosz/"&gt;Association for Software Testing 2023 elections proceedings&lt;/a&gt; I advocated for transparency. In the spirit of practising what I preach, this blog post summarizes my work for &lt;span class="caps"&gt;AST&lt;/span&gt; in January&amp;nbsp;2024.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Implemented &lt;span class="caps"&gt;DNS&lt;/span&gt; changes required for VeryConnect (software running our members&amp;nbsp;portal)&lt;/li&gt;
&lt;li&gt;Did about 75% of Phone.com migration. We want to stop using phone number from them and someone has to log in to all our accounts, check what phone number is set in profile and change it when necessary. While doing that I have identified some action items for other Board members and cost saving opportunities, which I need to write&amp;nbsp;down.&lt;/li&gt;
&lt;li&gt;Created runbook for accessing shared email accounts despite Google security mechanisms. As usual, this is internal&amp;nbsp;only.&lt;/li&gt;
&lt;li&gt;Started a discussion with Google Customer service for a problem where email sent to Group is not distributed to all members of that&amp;nbsp;Group.&lt;/li&gt;
&lt;li&gt;Monthly Board of Directors&amp;nbsp;meeting.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You might have noticed there was no report for December. That&amp;#8217;s because we have kicked off a month with quarterly Board of Directors meeting, which is much longer and much more demanding that monthly ones. With pretty busy end of the year at work and festive season, unfortunately I did not contribute anything to &lt;span class="caps"&gt;AST&lt;/span&gt; that&amp;nbsp;month.&lt;/p&gt;
&lt;p&gt;If you are interested in becoming a better tester, advancing the practice of software testing or just want to support my work, consider &lt;a href="http://eepurl.com/tCFsn"&gt;subscribing to &lt;span class="caps"&gt;AST&lt;/span&gt; newsletter&lt;/a&gt; and &lt;a href="https://associationforsoftwaretesting.org/about-the-ast-a-professional-body-for-testers/membership/become-a-member-of-the-the-association-for-software-testing/"&gt;becoming a member&lt;/a&gt;. If you have any questions, ideas or would like to work together, you can email me&amp;nbsp;at &lt;code&gt;mirekdlugosz %at% associationforsoftwaretesting.org&lt;/code&gt; or &lt;a href="https://mirekdlugosz.com/contact.html"&gt;get in touch through other channels&lt;/a&gt;.&lt;/p&gt;</content><category term="Blog"/><category term="AST"/><category term="planet AST"/><category term="planet MoT"/></entry><entry><title>When the build is green, the product is of sufficient quality to release</title><link href="https://mirekdlugosz.com/blog/2024/when-the-build-is-green-the-product-is-of-sufficient-quality-to-release/" rel="alternate"/><published>2024-01-25T23:08:31+01:00</published><updated>2024-01-25T23:08:31+01:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2024-01-25:/blog/2024/when-the-build-is-green-the-product-is-of-sufficient-quality-to-release/</id><summary type="html">&lt;p&gt;The Association for Software Testing is crowd-sourcing a book, &lt;a href="https://associationforsoftwaretesting.org/navigating-the-world-as-a-context-driven-tester-book/"&gt;&lt;em&gt;Navigating the World as a Context-Driven Tester&lt;/em&gt;&lt;/a&gt;. The book is edited by &lt;a href="https://therockertester.wordpress.com/"&gt;Lee Hawkins&lt;/a&gt;, who posts questions on &lt;a href="https://twitter.com/AST_News"&gt;Twitter&lt;/a&gt;, &lt;a href="https://www.linkedin.com/company/association-for-software-testing/"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://sw-development-is.social/web/@AST"&gt;Mastodon&lt;/a&gt;, &lt;a href="https://associationforsoftwaretesting.org/2016/11/13/ast-members-slack/"&gt;Slack&lt;/a&gt;, and &lt;a href="http://eepurl.com/tCFsn"&gt;the &lt;span class="caps"&gt;AST&lt;/span&gt; mailing list&lt;/a&gt;. You don&amp;#8217;t have to be a member to give your answer to the …&lt;/p&gt;</summary><content type="html">&lt;p&gt;The Association for Software Testing is crowd-sourcing a book, &lt;a href="https://associationforsoftwaretesting.org/navigating-the-world-as-a-context-driven-tester-book/"&gt;&lt;em&gt;Navigating the World as a Context-Driven Tester&lt;/em&gt;&lt;/a&gt;. The book is edited by &lt;a href="https://therockertester.wordpress.com/"&gt;Lee Hawkins&lt;/a&gt;, who posts questions on &lt;a href="https://twitter.com/AST_News"&gt;Twitter&lt;/a&gt;, &lt;a href="https://www.linkedin.com/company/association-for-software-testing/"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://sw-development-is.social/web/@AST"&gt;Mastodon&lt;/a&gt;, &lt;a href="https://associationforsoftwaretesting.org/2016/11/13/ast-members-slack/"&gt;Slack&lt;/a&gt;, and &lt;a href="http://eepurl.com/tCFsn"&gt;the &lt;span class="caps"&gt;AST&lt;/span&gt; mailing list&lt;/a&gt;. You don&amp;#8217;t have to be a member to give your answer to the most recent prompt, &amp;#8220;When the build is green, the product is of sufficient quality to release&amp;#8221;. Mine is&amp;nbsp;below.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;No, not really. At least not in any general sense. But you are free to choose to release based on single metric. If this is what you want, I would advise you to work on creating an environment that allows for this choice to succeed. I would also assume that you are aware of risks that this option entails, and you have considered other options as&amp;nbsp;well.&lt;/p&gt;
&lt;p&gt;If you want to release a software, you need a release strategy. You don&amp;#8217;t have to call it like that and you don&amp;#8217;t have to write it down, but it makes sense to spend few hours thinking about it. Here are some questions you should be able to&amp;nbsp;answer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How many releases do you want to make? When you work under contract, there might be set number of releases that you will be able to make. Some specialized software is being used only once - think about space rocket controls, or software used during sports events like&amp;nbsp;Olympics.&lt;/li&gt;
&lt;li&gt;How often do you want to release? Assuming there are going to be multiple releases, is there a specific frequency of releases that you want to maintain? Or are you going to release when you feel you are&amp;nbsp;ready?&lt;/li&gt;
&lt;li&gt;Are you able to fix discovered problems after the release? Especially if you don&amp;#8217;t plan multiple releases, would you be able to release fixes for critical problems discovered&amp;nbsp;afterwards?&lt;/li&gt;
&lt;li&gt;What are the risks of releasing software with unfixed problems? This greatly depends on the nature of these problems, but think also about your reputation and&amp;nbsp;competition.&lt;/li&gt;
&lt;li&gt;What are the risks of holding a release to fix problems? This is mostly about all the things accompanying and related to release, e.g. marketing campaign that started well before the planned release date. Can you move things around? Can you afford to extend&amp;nbsp;it?&lt;/li&gt;
&lt;li&gt;What does it take to actually make a release? All the technical aspects of getting the software to customers, including distribution logistics for physical copies, if there are&amp;nbsp;any.&lt;/li&gt;
&lt;li&gt;What external factors might force you to make a release? These might be technical, like new version of operating system, or related to changed laws and&amp;nbsp;regulations.&lt;/li&gt;
&lt;li&gt;How much in advance will you know about these external factors? Changes in laws are usually announced months or years in advance, but you might have only weeks to respond to changes in operating&amp;nbsp;system.&lt;/li&gt;
&lt;li&gt;How does a release align with other things that company is doing? Your software is probably only a part of the company offering. Are you independent of other projects? Even when your software is the only thing that company provides, there are probably other branches in company that support it and they all have to work with common goal in&amp;nbsp;mind.&lt;/li&gt;
&lt;li&gt;Who decides to make a release? Is that a development team, a product manager, a branch director, sales people? Software can&amp;#8217;t release itself, so who decides that this specific version is going to be made available to&amp;nbsp;customers?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can decide to empower the team to make release decisions whenever they see fit, disregarding most of outside factors and concerns. That&amp;#8217;s a perfectly valid&amp;nbsp;choice!&lt;/p&gt;
&lt;p&gt;The common risk of every single software release is that there might be important problems that you don&amp;#8217;t know about, and that you underestimated problems that you do know about - that they turn out to be more serious than you thought. By making a choice to release a new version based only on build results, you decide that these and some other risks are acceptable for&amp;nbsp;you.&lt;/p&gt;
&lt;p&gt;First set of risks revolves around quality of your automated checks suite that is run before the release. Common problems with such suites are lack of data variability and coverage gaps in areas that are hard to test, especially interactions with real external systems. If you don&amp;#8217;t maintain the discipline of only merging code that is covered by tests, you might also have insufficient coverage of newer features. Automated checks suite must also maintain a balance between thoroughness and execution time. It is common to keep separate pipelines for slow performance and security testing, and there&amp;#8217;s a risk of forgetting to run them before making a&amp;nbsp;release.&lt;/p&gt;
&lt;p&gt;If you make it easy to make a release, and you make a habit of frequent releases, there&amp;#8217;s a risk of losing vigilance. A Big Important Release acts as external pressure that keeps everyone cautious. When releases become a common thing, you might underestimate the importance of found issues, working under the assumption that you can always fix them later and make a new release. That assumption is true and valid for many software issues, but it will become a problem when it starts to be an excuse for&amp;nbsp;sloppiness.&lt;/p&gt;
&lt;p&gt;When release decision is made based on build results, releases are usually more frequent and smaller in scope (contain a limited number of changes). The argument goes, smaller releases are easier to test and inherently less risky, so it&amp;#8217;s better to release more frequently. On the other hand, smaller changes carry a risk of losing the forest for the trees. As your team is focused on small tasks, there might be nobody to consider the impact of these small changes on the product as a whole. Things that work well in isolation sometimes do not work well&amp;nbsp;together.&lt;/p&gt;
&lt;p&gt;Finally, there&amp;#8217;s a risk of that choice being wrong for your actual case. Many developers appreciate easy, small and frequent releases. But most of your customers just want to get the task done - they don&amp;#8217;t want to wait for update or be interrupted by it. They might have slow Internet connection or pay for downloaded data. They might be averse to change - people tend to be conservative and bear with arduous way of doing things, just because they have always done it like&amp;nbsp;that.&lt;/p&gt;
&lt;p&gt;If you want to release based on build results, you want to ensure that you have other ways to minimize these risks, and you have a safety net that minimizes the impact of them when they inevitably&amp;nbsp;occur.&lt;/p&gt;
&lt;p&gt;First, make it technically easy to release a new version of software. That usually means that release pipeline is fully automated. Whenever you decide to make a release, you should be able to just push a button and be confident that new version is available to&amp;nbsp;customers.&lt;/p&gt;
&lt;p&gt;Second, prepare a plan for when broken version is released, or release process itself fails. That might mean a simple way to yank and roll back a release, or empowering a team to quickly fix an issue and make another release. You might also schedule fire drills when you pretend that working release is broken, just to prepare for a real&amp;nbsp;situation.&lt;/p&gt;
&lt;p&gt;Third, if your delivery process allows it, consider rolling releases, blue/green deployments and post-release monitoring. The general idea is that you release new version to some small subset of your customers, and gradually make it available to others. That should be paired with monitoring, so if you notice that there are some problems with new version, you pause the release. This way most of customers never experience the broken&amp;nbsp;behavior.&lt;/p&gt;
&lt;p&gt;Finally, you need the ability to deliver new versions frequently. This is a given when you work on a software as a service, where you fully control who uses which version. But this dynamic changes as you move to software that is installed on customer devices, especially devices that are not always&amp;nbsp;online.&lt;/p&gt;
&lt;p&gt;To sum up, the quality of software, as signaled by build status, is only a small part of release equation. There&amp;#8217;s nothing wrong in deciding to focus on it, disregarding all the other factors. But you need to ensure that this is the right choice in your general business environment, and you should work on safety net that minimises the risk that this choice&amp;nbsp;entails.&lt;/p&gt;</content><category term="Blog"/><category term="AST"/><category term="planet AST"/><category term="planet MoT"/><category term="testing"/></entry><entry><title>Playwright - accessing page object in event handler</title><link href="https://mirekdlugosz.com/blog/2024/playwright-accessing-page-object-in-event-handler/" rel="alternate"/><published>2024-01-03T18:37:03+01:00</published><updated>2024-01-03T18:37:03+01:00</updated><author><name>Mirek Długosz</name></author><id>tag:mirekdlugosz.com,2024-01-03:/blog/2024/playwright-accessing-page-object-in-event-handler/</id><summary type="html">&lt;p&gt;Playwright &lt;a href="https://playwright.dev/python/docs/api/class-page#events"&gt;exposes a number of browser events&lt;/a&gt; and provides a mechanism to respond to them. Since many of these events signal errors and problems, most of the time you want to log them, halt program execution, or ignore and move on. Logging is also shown in &lt;a href="https://playwright.dev/python/docs/network#network-events"&gt;Playwright documentation about network …&lt;/a&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;Playwright &lt;a href="https://playwright.dev/python/docs/api/class-page#events"&gt;exposes a number of browser events&lt;/a&gt; and provides a mechanism to respond to them. Since many of these events signal errors and problems, most of the time you want to log them, halt program execution, or ignore and move on. Logging is also shown in &lt;a href="https://playwright.dev/python/docs/network#network-events"&gt;Playwright documentation about network&lt;/a&gt;, which I will use as a base for examples in this&amp;nbsp;article.&lt;/p&gt;
&lt;h2 id="problem-statement"&gt;&lt;a class="toclink" href="#problem-statement"&gt;Problem&amp;nbsp;statement&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Documentation shows event handlers created&amp;nbsp;with &lt;code&gt;lambda&lt;/code&gt; expressions,&amp;nbsp;but &lt;code&gt;lambda&lt;/code&gt; poses significant problems once you leave the territory of toy&amp;nbsp;examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;they should fit in single line of&amp;nbsp;code&lt;/li&gt;
&lt;li&gt;you can&amp;#8217;t share them across&amp;nbsp;modules&lt;/li&gt;
&lt;li&gt;you can&amp;#8217;t unit test them in&amp;nbsp;isolation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Usually you want to define event handlers as normal functions. But when you attempt that, you might run into another problem - Playwright invokes event handler with some event-related data, that data does not contain any reference back&amp;nbsp;to &lt;code&gt;page&lt;/code&gt; object,&amp;nbsp;and &lt;code&gt;page&lt;/code&gt; object might contain some important contextual&amp;nbsp;information.&lt;/p&gt;
&lt;p&gt;In other words, we would like to do something similar to code below. Note that this example does not work - if you run it, you will&amp;nbsp;get &lt;code&gt;NameError: name 'page' is not defined&lt;/code&gt;. &lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;playwright.sync_api&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;playwright.sync_api&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Playwright&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;request_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; issued request: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;response_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; received response: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;run_test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playwright&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Playwright&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;playwright&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;https://mirekdlugosz.com&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;request&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request_handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;response&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;https://httpbin.org/status/404&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;playwright&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;run_test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playwright&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I can think of three ways of solving that: by defining a function inside a function,&amp;nbsp;with &lt;code&gt;functools.partial&lt;/code&gt;and with a factory function. Let&amp;#8217;s take a look at all of&amp;nbsp;them.&lt;/p&gt;
&lt;h2 id="defining-a-function-inside-a-function"&gt;&lt;a class="toclink" href="#defining-a-function-inside-a-function"&gt;Defining a function inside a&amp;nbsp;function&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Most Python users are so used to defining functions at the top level of module or inside a class (we call these &amp;#8220;methods&amp;#8221;) that they might consider function definitions to be somewhat special. In fact, some other programming languages do encumber where functions can be defined. But in Python you can define them anywhere, including inside other&amp;nbsp;functions.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;playwright.sync_api&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;playwright.sync_api&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Playwright&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;run_test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playwright&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Playwright&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;request_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; issued request: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;response_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; received response: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


    &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;playwright&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;https://mirekdlugosz.com&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;request&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request_handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;response&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;https://httpbin.org/status/404&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;playwright&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;run_test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playwright&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This works because &lt;a href="https://docs.python.org/3/reference/compound_stmts.html#function-definitions"&gt;function body is not evaluated until function is called&lt;/a&gt; and &lt;a href="https://peps.python.org/pep-0227/"&gt;functions have access to names defined in their encompassing scope&lt;/a&gt;. So Python will look&amp;nbsp;up &lt;code&gt;page&lt;/code&gt; only when event handler is invoked by Playwright; since it&amp;#8217;s not defined in function itself, Python will look for it in the function where event handler was defined (and then next function, if there is one, then module and eventually&amp;nbsp;builtins).&lt;/p&gt;
&lt;p&gt;I think this solution solves the most important part of the problem - it allows to write event handlers that span multiple lines. Technically it is also possible to share these handlers across modules, but you won&amp;#8217;t see that often. They can&amp;#8217;t be unit tested in isolation, as they depend on their parent&amp;nbsp;function.&lt;/p&gt;
&lt;h2 id="functoolspartial"&gt;&lt;a class="toclink" href="#functoolspartial"&gt;&lt;code&gt;functools.partial&lt;/code&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://docs.python.org/3/library/functools.html#functools.partial"&gt;&lt;code&gt;functools.partial&lt;/code&gt; documentation&lt;/a&gt; may be confusing, as prose sounds exactly like a description of standard function, code equivalent assumes pretty good understanding of Python internals, and provided example seems completely&amp;nbsp;unnecessary.&lt;/p&gt;
&lt;p&gt;I think&amp;nbsp;about &lt;code&gt;partial&lt;/code&gt; this way: it creates a function that has some of the arguments already filled&amp;nbsp;in.&lt;/p&gt;
&lt;p&gt;To be&amp;nbsp;fair, &lt;code&gt;partial&lt;/code&gt; is rarely &lt;em&gt;needed&lt;/em&gt;. It allows to write shorter code, as you don&amp;#8217;t have to repeat the same arguments over and over again. It may also allow you to provide saner library &lt;span class="caps"&gt;API&lt;/span&gt; - you can define single generic and flexible function with a lot of arguments, and few helper functions intended for external use, each with a small number of&amp;nbsp;arguments.&lt;/p&gt;
&lt;p&gt;But it&amp;#8217;s invaluable when you have to provide your own function, but you don&amp;#8217;t have control over arguments it will receive. Which is &lt;em&gt;exactly&lt;/em&gt; the problem we are&amp;nbsp;facing.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;functools&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;partial&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;playwright.sync_api&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;playwright.sync_api&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Playwright&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;request_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; issued request: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;response_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; received response: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;run_test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playwright&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Playwright&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;playwright&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;https://mirekdlugosz.com&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;local_request_handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request_handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;local_response_handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response_handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;request&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;local_request_handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;response&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;local_response_handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;https://httpbin.org/status/404&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;playwright&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;run_test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playwright&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Notice that our function takes the same arguments as Playwright event handler, and then some. When it&amp;#8217;s time to assign event handlers, we&amp;nbsp;use &lt;code&gt;partial&lt;/code&gt; to create a new function, one that only needs argument that we will receive from Playwright - the other one is already filled in. But when function is executed, it will receive both&amp;nbsp;arguments.&lt;/p&gt;
&lt;h2 id="factory-function"&gt;&lt;a class="toclink" href="#factory-function"&gt;Factory&amp;nbsp;function&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Functions in Python may not only define other functions in their bodies, but also return functions. They are called &amp;#8220;higher-order functions&amp;#8221; and aren&amp;#8217;t used often, with one notable exception of &lt;a href="https://realpython.com/primer-on-python-decorators/"&gt;decorators&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;playwright.sync_api&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;playwright.sync_api&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Playwright&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;request_handler_factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; issued request: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;inner&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;response_handler_factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; received response: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;inner&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;run_test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playwright&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Playwright&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;playwright&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;https://mirekdlugosz.com&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;request&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request_handler_factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;response&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_handler_factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;https://httpbin.org/status/404&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;playwright&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;run_test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playwright&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The key here is that inner function has access to all of enclosing scope, including values passed as arguments to outer function. This allows us to pass specific values that are only available in the place where outer function is&amp;nbsp;called.&lt;/p&gt;
&lt;h2 id="summary"&gt;&lt;a class="toclink" href="#summary"&gt;Summary&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The first solution is a little different than other two, because it does not solve all of the problems set forth. On the other hand, I think it&amp;#8217;s the easiest to understand - even beginner Python programmers should intuitively grasp what is happening and&amp;nbsp;why. &lt;/p&gt;
&lt;p&gt;In my experience higher-order functions takes some getting used to,&amp;nbsp;while &lt;code&gt;partial&lt;/code&gt; is not well-known and may be confusing at first. But they do solve our problem&amp;nbsp;completely.&lt;/p&gt;</content><category term="Blog"/><category term="planet AST"/><category term="planet MoT"/><category term="planet Python"/><category term="Python"/><category term="testing"/><category term="tutorial"/></entry></feed>