<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<?xml-stylesheet type="text/xsl" href="/rss.xsl"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Andrey Listopadov</title>
    <link>https://andreyor.st/categories/testing/</link>
    <image>
      <title>Andrey Listopadov</title>
      <link>https://andreyor.st/categories/testing/</link>
      <url>https://andreyor.st/categories/testing/favicon-64.png</url>
    </image>
    <description>Posts from the 'testing' category by Andrey Listopadov</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <copyright>Andrey Listopadov 2020-2026 - This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License</copyright>
    <lastBuildDate>Wed, 15 Apr 2026 05:11:00 +0300</lastBuildDate>
    <atom:link href="https://andreyor.st/categories/testing/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Clojure on Fennel part two: immutable.fnl optimizations</title>
      <link>https://andreyor.st/posts/2026-04-15-clojure-on-fennel-part-two-immutablefnl-optimizations/</link>
      <guid>https://andreyor.st/posts/2026-04-15-clojure-on-fennel-part-two-immutablefnl-optimizations/</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://andreyor.st/posts/2026-04-07-clojure-on-fennel-part-one-persistent-data-structures/&#34;&gt;part one: persistent data structures&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;part two: immutable.fnl optimizations&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://andreyor.st/posts/2026-04-27-clojure-on-fennel-part-three-parsing/&#34;&gt;part three: parsing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://andreyor.st/posts/2026-06-29-clojure-on-fennel-part-four-parsing-again/&#34;&gt;part four: parsing (again)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;So the next post will hopefully be about the compiler itself.&lt;/p&gt;
&lt;p&gt;Unless I get distracted again.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Sike!&lt;/p&gt;
&lt;p&gt;While I did some work on the compiler, I&amp;rsquo;m not feeling ready to talk about it yet.
I want the post about it to be, well, more in-depth, so for now, let&amp;rsquo;s go back to immutable data structures.
I got some comments on their performance, which can be summarized to &amp;ldquo;bad&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;And I agree!
The performance is not great, especially when compared to plain Lua tables.
However, my benchmarking process was a bit flawed, and I&amp;rsquo;ve improved it since.&lt;/p&gt;
&lt;p&gt;This post won&amp;rsquo;t be long, but I wanted to make it anyway, to illustrate what I&amp;rsquo;m dealing with.
Again, this is not the final version, I&amp;rsquo;m still working on possible optimizations as I write this, but some things have improved already, though not by much.&lt;/p&gt;
&lt;p&gt;There were some low-hanging fruits that I could tackle:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vector had a local &lt;code&gt;fragment&lt;/code&gt; function with pure arithmetic.
I already have the same &lt;code&gt;fragment&lt;/code&gt; function in my bitops module that uses native rshift+band.
The modulo operation is expensive.&lt;/li&gt;
&lt;li&gt;A similar case of using bit operations instead of modulo was in HashMap&amp;rsquo;s &lt;code&gt;index&lt;/code&gt; function.&lt;/li&gt;
&lt;li&gt;Increased the popcount table to 256 entries instead of 16.
Halves the loop iterations for typical bitmaps.&lt;/li&gt;
&lt;li&gt;Probably premature, but replaced most instances of &lt;code&gt;faccumulate&lt;/code&gt;, &lt;code&gt;fcollect&lt;/code&gt;, etc. with plain &lt;code&gt;for&lt;/code&gt; loops.
These loops are quite hot, and Fennel compiles them to have additional assignment at each iteration.
Should be free, but actually had a small impact.&lt;/li&gt;
&lt;li&gt;More pre-allocations when creating Lua tables.
Using &lt;code&gt;[nil nil nil nil]&lt;/code&gt; tells the Lua compiler to pre-allocate four slots when creating the table, meaning it won&amp;rsquo;t resize once we insert elements.
Tables in Lua resize by a factor of 2 and copy everything on resize, which is not great for performance.&lt;/li&gt;
&lt;li&gt;Vector iterator is now stateful.
And also fast.&lt;/li&gt;
&lt;li&gt;Internal fields for data structures used long, elaborate keys, which meant string comparison in certain cases of table lookup would be unnecessarily long.
Again, changing them to shorter keys should not matter that much, but it did have some impact.&lt;/li&gt;
&lt;li&gt;HashMap branching factor was increased to 32.
After some tests, it proved to be slightly better, despite what I said in my previous post.
More on that later.&lt;/li&gt;
&lt;li&gt;Hashes are now cached in a weak table.
Helps avoid re-hashing the same values at the expense of memory.
Not sure how well it will scale.&lt;/li&gt;
&lt;li&gt;Optimized array copying for sizes less than 4 by doing direct table construction.
Avoids loop overhead.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With these changes in place (and some more I omitted), here are some of the benchmarks:&lt;/p&gt;
&lt;figure class=&#34;invertable&#34;&gt;&lt;img src=&#34;https://andreyor.st/2026-04-15-clojure-on-fennel-part-two-immutablefnl-optimizations/lua.png&#34;/&gt;
&lt;/figure&gt;

&lt;p&gt;These images show per-commit changes to the performance.
Really helped me track what was worth keeping, and what wasn&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the LuaJIT version of the benchmark:&lt;/p&gt;
&lt;figure class=&#34;invertable&#34;&gt;&lt;img src=&#34;https://andreyor.st/2026-04-15-clojure-on-fennel-part-two-immutablefnl-optimizations/luajit.png&#34;/&gt;
&lt;/figure&gt;

&lt;p&gt;Here&amp;rsquo;s another graph:&lt;/p&gt;
&lt;figure class=&#34;invertable&#34;&gt;&lt;img src=&#34;https://andreyor.st/2026-04-15-clojure-on-fennel-part-two-immutablefnl-optimizations/hashmap.png&#34;/&gt;
&lt;/figure&gt;

&lt;p&gt;This is me testing how hash map branching factor affects operations.
As can be seen, switching from 16 to 32 makes inserts slower on PUC Lua 5.5 but makes indexing faster.
On LuaJIT both operations are faster.&lt;/p&gt;
&lt;figure class=&#34;invertable&#34;&gt;&lt;img src=&#34;https://andreyor.st/2026-04-15-clojure-on-fennel-part-two-immutablefnl-optimizations/insertion-lua.png&#34;/&gt;
&lt;/figure&gt;

&lt;p&gt;Looking at the charts, the &lt;code&gt;7989c32&lt;/code&gt; commit with that change (the second orange bar) — insertions are slower than the previous commit, and indexing is faster than the previous commit.
Unfortunately, LuaJIT charts are noisy and I can&amp;rsquo;t confirm the speedup there.
Something to do with instabilities in the LuaJIT tracer, I guess.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; I know, the results for some operations are worse than before all optimizations that took place.
This is still a work in progress.
Just shows how important relative measuring is.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The results are a mixed bag.
Most operations that improved did so by only 1 or 2 milliseconds overall, meaning the per-operation cost is still quite high, compared to Lua tables.
Yes, some became faster, so I&amp;rsquo;m keeping these changes, but I can also see that for some operations, they actually got slower than they were before any optimization work.
The reason for that may be that I fixed a few bugs along the way.
Anyway, I tried to prioritize transient collection variants, as they benefit most from optimizations given how they&amp;rsquo;re used, and LuaJIT performance where possible.&lt;/p&gt;
&lt;p&gt;Some bars are not meaningful for certain operations.
E.g., I had changes that had nothing to do with Persistent Vector, but affected it anyway, so this benchmark is still a bit flawed.
At least those differences are within the deviation range.&lt;/p&gt;
&lt;p&gt;Anyhow, just wanted to show that optimization is hard, and not all changes yield the results you expect.
In the case of this library, I don&amp;rsquo;t think there are more low-hanging fruits to optimize, or at least I don&amp;rsquo;t see any.
For example, there are a lot of table allocations that are hard to get rid of in the current implementation.
A deeper analysis is needed, probably by people with more Lua knowledge.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m still satisfied with some improvements I got, but we&amp;rsquo;ll see if more are on the way.&lt;/p&gt;
&lt;h2 id=&#34;update&#34;&gt;Update&lt;/h2&gt;
&lt;p&gt;After a lot more benchmarks, flamegraphs, and throwing stuff at the wall, I think I can&amp;rsquo;t do much more.
I decided to update this post instead of writing another one (and really should have waited before publishing the original) because at that point, I thought there was nothing more to do.
Now I think there really isn&amp;rsquo;t.
So the results are in:&lt;/p&gt;
&lt;figure class=&#34;invertable&#34;&gt;&lt;img src=&#34;https://andreyor.st/2026-04-15-clojure-on-fennel-part-two-immutablefnl-optimizations/lua-final.png&#34;
         alt=&#34;Figure 1: PUC Lua 5.5 (green - before, orange - after)&#34;/&gt;&lt;figcaption&gt;
            &lt;p&gt;&lt;span class=&#34;figure-number&#34;&gt;Figure 1: &lt;/span&gt;PUC Lua 5.5 (green - before, orange - after)&lt;/p&gt;
        &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure class=&#34;invertable&#34;&gt;&lt;img src=&#34;https://andreyor.st/2026-04-15-clojure-on-fennel-part-two-immutablefnl-optimizations/luajit-final.png&#34;
         alt=&#34;Figure 2: LuaJIT (green - before, orange - after)&#34;/&gt;&lt;figcaption&gt;
            &lt;p&gt;&lt;span class=&#34;figure-number&#34;&gt;Figure 2: &lt;/span&gt;LuaJIT (green - before, orange - after)&lt;/p&gt;
        &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;As can be seen, the changes are more dramatic on Lua than on LuaJIT.
Mostly because it&amp;rsquo;s hard to optimize for LuaJIT without sacrificing performance on Lua, so I re-prioritized Lua, since LuaJIT is already much faster.&lt;/p&gt;
&lt;p&gt;The final benchmark tables are below.&lt;/p&gt;
&lt;h4 id=&#34;puc-lua-5-dot-5&#34;&gt;PUC Lua 5.5&lt;/h4&gt;
&lt;!--list-separator--&gt;
&lt;ul&gt;
&lt;li&gt;PersistentVector
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Lua table&lt;/th&gt;
&lt;th&gt;Persistent&lt;/th&gt;
&lt;th&gt;ratio&lt;/th&gt;
&lt;th&gt;per op&lt;/th&gt;
&lt;th&gt;ratio (old)&lt;/th&gt;
&lt;th&gt;per op (old)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;build 50000&lt;/td&gt;
&lt;td&gt;0.18 ms&lt;/td&gt;
&lt;td&gt;18.88 ms&lt;/td&gt;
&lt;td&gt;102x&lt;/td&gt;
&lt;td&gt;0.378 us&lt;/td&gt;
&lt;td&gt;119x&lt;/td&gt;
&lt;td&gt;0.440 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lookup random&lt;/td&gt;
&lt;td&gt;0.48 ms&lt;/td&gt;
&lt;td&gt;11.44 ms&lt;/td&gt;
&lt;td&gt;24x&lt;/td&gt;
&lt;td&gt;0.229 us&lt;/td&gt;
&lt;td&gt;28x&lt;/td&gt;
&lt;td&gt;0.266 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;assoc random&lt;/td&gt;
&lt;td&gt;0.31 ms&lt;/td&gt;
&lt;td&gt;42.72 ms&lt;/td&gt;
&lt;td&gt;139x&lt;/td&gt;
&lt;td&gt;0.854 us&lt;/td&gt;
&lt;td&gt;222x&lt;/td&gt;
&lt;td&gt;1.4 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pop all&lt;/td&gt;
&lt;td&gt;0.14 ms&lt;/td&gt;
&lt;td&gt;14.56 ms&lt;/td&gt;
&lt;td&gt;106x&lt;/td&gt;
&lt;td&gt;0.291 us&lt;/td&gt;
&lt;td&gt;150x&lt;/td&gt;
&lt;td&gt;0.415 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iterate&lt;/td&gt;
&lt;td&gt;0.60 ms&lt;/td&gt;
&lt;td&gt;2.74 ms&lt;/td&gt;
&lt;td&gt;5x&lt;/td&gt;
&lt;td&gt;0.055 us&lt;/td&gt;
&lt;td&gt;17x&lt;/td&gt;
&lt;td&gt;0.196 us&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;!--list-separator--&gt;
&lt;ul&gt;
&lt;li&gt;TransientVector
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Lua table&lt;/th&gt;
&lt;th&gt;Transient&lt;/th&gt;
&lt;th&gt;ratio&lt;/th&gt;
&lt;th&gt;per op&lt;/th&gt;
&lt;th&gt;ratio (old)&lt;/th&gt;
&lt;th&gt;per op (old)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;build 50000&lt;/td&gt;
&lt;td&gt;0.19 ms&lt;/td&gt;
&lt;td&gt;6.55 ms&lt;/td&gt;
&lt;td&gt;35x&lt;/td&gt;
&lt;td&gt;0.131 us&lt;/td&gt;
&lt;td&gt;41x&lt;/td&gt;
&lt;td&gt;0.154 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;assoc random&lt;/td&gt;
&lt;td&gt;0.31 ms&lt;/td&gt;
&lt;td&gt;17.64 ms&lt;/td&gt;
&lt;td&gt;57x&lt;/td&gt;
&lt;td&gt;0.353 us&lt;/td&gt;
&lt;td&gt;67x&lt;/td&gt;
&lt;td&gt;0.408 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pop all&lt;/td&gt;
&lt;td&gt;0.14 ms&lt;/td&gt;
&lt;td&gt;5.74 ms&lt;/td&gt;
&lt;td&gt;42x&lt;/td&gt;
&lt;td&gt;0.115 us&lt;/td&gt;
&lt;td&gt;51x&lt;/td&gt;
&lt;td&gt;0.142 us&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;!--list-separator--&gt;
&lt;ul&gt;
&lt;li&gt;PersistentHashMap
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Lua table&lt;/th&gt;
&lt;th&gt;Persistent&lt;/th&gt;
&lt;th&gt;ratio&lt;/th&gt;
&lt;th&gt;per op&lt;/th&gt;
&lt;th&gt;ratio (old)&lt;/th&gt;
&lt;th&gt;per op (old)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;insert 50000&lt;/td&gt;
&lt;td&gt;2.01 ms&lt;/td&gt;
&lt;td&gt;81.44 ms&lt;/td&gt;
&lt;td&gt;41x&lt;/td&gt;
&lt;td&gt;1.6 us&lt;/td&gt;
&lt;td&gt;82x&lt;/td&gt;
&lt;td&gt;3.2 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lookup random&lt;/td&gt;
&lt;td&gt;0.92 ms&lt;/td&gt;
&lt;td&gt;34.03 ms&lt;/td&gt;
&lt;td&gt;37x&lt;/td&gt;
&lt;td&gt;0.681 us&lt;/td&gt;
&lt;td&gt;81x&lt;/td&gt;
&lt;td&gt;1.7 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;contains random&lt;/td&gt;
&lt;td&gt;1.82 ms&lt;/td&gt;
&lt;td&gt;33.08 ms&lt;/td&gt;
&lt;td&gt;18x&lt;/td&gt;
&lt;td&gt;0.662 us&lt;/td&gt;
&lt;td&gt;49x&lt;/td&gt;
&lt;td&gt;1.7 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dissoc all&lt;/td&gt;
&lt;td&gt;0.90 ms&lt;/td&gt;
&lt;td&gt;76.97 ms&lt;/td&gt;
&lt;td&gt;86x&lt;/td&gt;
&lt;td&gt;1.5 us&lt;/td&gt;
&lt;td&gt;187x&lt;/td&gt;
&lt;td&gt;3.3 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dissoc 10%&lt;/td&gt;
&lt;td&gt;0.18 ms&lt;/td&gt;
&lt;td&gt;9.81 ms&lt;/td&gt;
&lt;td&gt;56x&lt;/td&gt;
&lt;td&gt;2.0 us&lt;/td&gt;
&lt;td&gt;112x&lt;/td&gt;
&lt;td&gt;3.7 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iterate&lt;/td&gt;
&lt;td&gt;1.91 ms&lt;/td&gt;
&lt;td&gt;7.22 ms&lt;/td&gt;
&lt;td&gt;4x&lt;/td&gt;
&lt;td&gt;0.144 us&lt;/td&gt;
&lt;td&gt;4x&lt;/td&gt;
&lt;td&gt;0.146 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lookup structural keys&lt;/td&gt;
&lt;td&gt;0.14 ms&lt;/td&gt;
&lt;td&gt;2.86 ms&lt;/td&gt;
&lt;td&gt;21x&lt;/td&gt;
&lt;td&gt;0.572 us&lt;/td&gt;
&lt;td&gt;146x&lt;/td&gt;
&lt;td&gt;4.1 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;insert collision-heavy&lt;/td&gt;
&lt;td&gt;0.13 ms&lt;/td&gt;
&lt;td&gt;6.50 ms&lt;/td&gt;
&lt;td&gt;51x&lt;/td&gt;
&lt;td&gt;1.3 us&lt;/td&gt;
&lt;td&gt;134x&lt;/td&gt;
&lt;td&gt;3.5 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mixed 80/20&lt;/td&gt;
&lt;td&gt;1.92 ms&lt;/td&gt;
&lt;td&gt;53.67 ms&lt;/td&gt;
&lt;td&gt;28x&lt;/td&gt;
&lt;td&gt;1.1 us&lt;/td&gt;
&lt;td&gt;62x&lt;/td&gt;
&lt;td&gt;2.3 us&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;!--list-separator--&gt;
&lt;ul&gt;
&lt;li&gt;TransientHashMap
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Lua table&lt;/th&gt;
&lt;th&gt;Transient&lt;/th&gt;
&lt;th&gt;ratio&lt;/th&gt;
&lt;th&gt;per op&lt;/th&gt;
&lt;th&gt;ratio (old)&lt;/th&gt;
&lt;th&gt;per op (old)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;insert 50000&lt;/td&gt;
&lt;td&gt;2.02 ms&lt;/td&gt;
&lt;td&gt;38.91 ms&lt;/td&gt;
&lt;td&gt;19x&lt;/td&gt;
&lt;td&gt;0.778 us&lt;/td&gt;
&lt;td&gt;44x&lt;/td&gt;
&lt;td&gt;1.7 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lookup random&lt;/td&gt;
&lt;td&gt;1.14 ms&lt;/td&gt;
&lt;td&gt;35.56 ms&lt;/td&gt;
&lt;td&gt;31x&lt;/td&gt;
&lt;td&gt;0.711 us&lt;/td&gt;
&lt;td&gt;86x&lt;/td&gt;
&lt;td&gt;1.7 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dissoc all&lt;/td&gt;
&lt;td&gt;0.89 ms&lt;/td&gt;
&lt;td&gt;44.52 ms&lt;/td&gt;
&lt;td&gt;50x&lt;/td&gt;
&lt;td&gt;0.890 us&lt;/td&gt;
&lt;td&gt;112x&lt;/td&gt;
&lt;td&gt;2.0 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mixed 80/20&lt;/td&gt;
&lt;td&gt;4.39 ms&lt;/td&gt;
&lt;td&gt;44.87 ms&lt;/td&gt;
&lt;td&gt;10x&lt;/td&gt;
&lt;td&gt;0.897 us&lt;/td&gt;
&lt;td&gt;22x&lt;/td&gt;
&lt;td&gt;1.9 us&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;luajit&#34;&gt;LuaJIT&lt;/h4&gt;
&lt;!--list-separator--&gt;
&lt;ul&gt;
&lt;li&gt;PersistentVector
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Lua table&lt;/th&gt;
&lt;th&gt;Persistent&lt;/th&gt;
&lt;th&gt;ratio&lt;/th&gt;
&lt;th&gt;per op&lt;/th&gt;
&lt;th&gt;ratio (old)&lt;/th&gt;
&lt;th&gt;per op (old)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;build 50000&lt;/td&gt;
&lt;td&gt;0.11 ms&lt;/td&gt;
&lt;td&gt;8.12 ms&lt;/td&gt;
&lt;td&gt;71x&lt;/td&gt;
&lt;td&gt;0.162 us&lt;/td&gt;
&lt;td&gt;84x&lt;/td&gt;
&lt;td&gt;0.177 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lookup random&lt;/td&gt;
&lt;td&gt;0.06 ms&lt;/td&gt;
&lt;td&gt;0.67 ms&lt;/td&gt;
&lt;td&gt;11x&lt;/td&gt;
&lt;td&gt;0.013 us&lt;/td&gt;
&lt;td&gt;11x&lt;/td&gt;
&lt;td&gt;0.013 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;assoc random&lt;/td&gt;
&lt;td&gt;0.04 ms&lt;/td&gt;
&lt;td&gt;22.91 ms&lt;/td&gt;
&lt;td&gt;559x&lt;/td&gt;
&lt;td&gt;0.458 us&lt;/td&gt;
&lt;td&gt;731x&lt;/td&gt;
&lt;td&gt;0.585 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pop all&lt;/td&gt;
&lt;td&gt;0.02 ms&lt;/td&gt;
&lt;td&gt;10.77 ms&lt;/td&gt;
&lt;td&gt;718x&lt;/td&gt;
&lt;td&gt;0.215 us&lt;/td&gt;
&lt;td&gt;657x&lt;/td&gt;
&lt;td&gt;0.210 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iterate&lt;/td&gt;
&lt;td&gt;0.02 ms&lt;/td&gt;
&lt;td&gt;0.23 ms&lt;/td&gt;
&lt;td&gt;12x&lt;/td&gt;
&lt;td&gt;0.005 us&lt;/td&gt;
&lt;td&gt;26x&lt;/td&gt;
&lt;td&gt;0.011 us&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;!--list-separator--&gt;
&lt;ul&gt;
&lt;li&gt;TransientVector
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Lua table&lt;/th&gt;
&lt;th&gt;Transient&lt;/th&gt;
&lt;th&gt;ratio&lt;/th&gt;
&lt;th&gt;per op&lt;/th&gt;
&lt;th&gt;ratio (old)&lt;/th&gt;
&lt;th&gt;per op (old)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;build 50000&lt;/td&gt;
&lt;td&gt;0.05 ms&lt;/td&gt;
&lt;td&gt;0.79 ms&lt;/td&gt;
&lt;td&gt;16x&lt;/td&gt;
&lt;td&gt;0.016 us&lt;/td&gt;
&lt;td&gt;12x&lt;/td&gt;
&lt;td&gt;0.012 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;assoc random&lt;/td&gt;
&lt;td&gt;0.04 ms&lt;/td&gt;
&lt;td&gt;2.22 ms&lt;/td&gt;
&lt;td&gt;55x&lt;/td&gt;
&lt;td&gt;0.044 us&lt;/td&gt;
&lt;td&gt;28x&lt;/td&gt;
&lt;td&gt;0.022 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pop all&lt;/td&gt;
&lt;td&gt;0.02 ms&lt;/td&gt;
&lt;td&gt;0.35 ms&lt;/td&gt;
&lt;td&gt;23x&lt;/td&gt;
&lt;td&gt;0.007 us&lt;/td&gt;
&lt;td&gt;50x&lt;/td&gt;
&lt;td&gt;0.016 us&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;!--list-separator--&gt;
&lt;ul&gt;
&lt;li&gt;PersistentHashMap
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Lua table&lt;/th&gt;
&lt;th&gt;Persistent&lt;/th&gt;
&lt;th&gt;ratio&lt;/th&gt;
&lt;th&gt;per op&lt;/th&gt;
&lt;th&gt;ratio (old)&lt;/th&gt;
&lt;th&gt;per op (old)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;insert 50000&lt;/td&gt;
&lt;td&gt;0.79 ms&lt;/td&gt;
&lt;td&gt;32.32 ms&lt;/td&gt;
&lt;td&gt;41x&lt;/td&gt;
&lt;td&gt;0.646 us&lt;/td&gt;
&lt;td&gt;50x&lt;/td&gt;
&lt;td&gt;0.989 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lookup random&lt;/td&gt;
&lt;td&gt;0.34 ms&lt;/td&gt;
&lt;td&gt;5.78 ms&lt;/td&gt;
&lt;td&gt;17x&lt;/td&gt;
&lt;td&gt;0.116 us&lt;/td&gt;
&lt;td&gt;43x&lt;/td&gt;
&lt;td&gt;0.248 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;contains random&lt;/td&gt;
&lt;td&gt;0.30 ms&lt;/td&gt;
&lt;td&gt;5.91 ms&lt;/td&gt;
&lt;td&gt;20x&lt;/td&gt;
&lt;td&gt;0.118 us&lt;/td&gt;
&lt;td&gt;40x&lt;/td&gt;
&lt;td&gt;0.257 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dissoc all&lt;/td&gt;
&lt;td&gt;0.23 ms&lt;/td&gt;
&lt;td&gt;27.02 ms&lt;/td&gt;
&lt;td&gt;119x&lt;/td&gt;
&lt;td&gt;0.540 us&lt;/td&gt;
&lt;td&gt;191x&lt;/td&gt;
&lt;td&gt;0.894 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dissoc 10%&lt;/td&gt;
&lt;td&gt;0.05 ms&lt;/td&gt;
&lt;td&gt;5.89 ms&lt;/td&gt;
&lt;td&gt;118x&lt;/td&gt;
&lt;td&gt;1.2 us&lt;/td&gt;
&lt;td&gt;121x&lt;/td&gt;
&lt;td&gt;1.4 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iterate&lt;/td&gt;
&lt;td&gt;0.07 ms&lt;/td&gt;
&lt;td&gt;1.07 ms&lt;/td&gt;
&lt;td&gt;16x&lt;/td&gt;
&lt;td&gt;0.021 us&lt;/td&gt;
&lt;td&gt;30x&lt;/td&gt;
&lt;td&gt;0.040 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lookup structural keys&lt;/td&gt;
&lt;td&gt;0.02 ms&lt;/td&gt;
&lt;td&gt;2.41 ms&lt;/td&gt;
&lt;td&gt;121x&lt;/td&gt;
&lt;td&gt;0.483 us&lt;/td&gt;
&lt;td&gt;119x&lt;/td&gt;
&lt;td&gt;0.498 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;insert collision-heavy&lt;/td&gt;
&lt;td&gt;0.28 ms&lt;/td&gt;
&lt;td&gt;5.20 ms&lt;/td&gt;
&lt;td&gt;19x&lt;/td&gt;
&lt;td&gt;1.0 us&lt;/td&gt;
&lt;td&gt;36x&lt;/td&gt;
&lt;td&gt;1.2 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mixed 80/20&lt;/td&gt;
&lt;td&gt;0.50 ms&lt;/td&gt;
&lt;td&gt;46.59 ms&lt;/td&gt;
&lt;td&gt;94x&lt;/td&gt;
&lt;td&gt;0.932 us&lt;/td&gt;
&lt;td&gt;40x&lt;/td&gt;
&lt;td&gt;0.513 us&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;!--list-separator--&gt;
&lt;ul&gt;
&lt;li&gt;TransientHashMap
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Lua table&lt;/th&gt;
&lt;th&gt;Transient&lt;/th&gt;
&lt;th&gt;ratio&lt;/th&gt;
&lt;th&gt;per op&lt;/th&gt;
&lt;th&gt;ratio (old)&lt;/th&gt;
&lt;th&gt;per op  (old)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;insert 50000&lt;/td&gt;
&lt;td&gt;0.91 ms&lt;/td&gt;
&lt;td&gt;30.92 ms&lt;/td&gt;
&lt;td&gt;34x&lt;/td&gt;
&lt;td&gt;0.618 us&lt;/td&gt;
&lt;td&gt;11x&lt;/td&gt;
&lt;td&gt;0.242 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lookup random&lt;/td&gt;
&lt;td&gt;0.32 ms&lt;/td&gt;
&lt;td&gt;34.90 ms&lt;/td&gt;
&lt;td&gt;110x&lt;/td&gt;
&lt;td&gt;0.698 us&lt;/td&gt;
&lt;td&gt;44x&lt;/td&gt;
&lt;td&gt;0.246 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dissoc all&lt;/td&gt;
&lt;td&gt;0.23 ms&lt;/td&gt;
&lt;td&gt;36.35 ms&lt;/td&gt;
&lt;td&gt;162x&lt;/td&gt;
&lt;td&gt;0.727 us&lt;/td&gt;
&lt;td&gt;57x&lt;/td&gt;
&lt;td&gt;0.271 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mixed 80/20&lt;/td&gt;
&lt;td&gt;1.29 ms&lt;/td&gt;
&lt;td&gt;40.71 ms&lt;/td&gt;
&lt;td&gt;32x&lt;/td&gt;
&lt;td&gt;0.814 us&lt;/td&gt;
&lt;td&gt;11x&lt;/td&gt;
&lt;td&gt;0.309 us&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Yes, the benchmark shows that some operations slowed down, some sped up.
Ideally, I&amp;rsquo;d like to have no slowdown, but that&amp;rsquo;s that.
Still, I wouldn&amp;rsquo;t trust my LuaJIT benchmark, though - graphs show that for some weird reason unrelated changes affect performance in the modules not touched by those changes at all.
So these changes are now merged into the main branch.&lt;/p&gt;
&lt;p&gt;Main bottleneck now is table allocation and array copying - things I already spent optimizing a lot of time with mixed results.
So I think I can&amp;rsquo;t do better than that.
At least without major algorithmic restructuring.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;With that&lt;/strong&gt;, the next post &lt;em&gt;should&lt;/em&gt; be about the compiler.&lt;/p&gt;
&lt;div class=&#34;comment-link&#34;&gt; &lt;a href=&#34;mailto:%61%6e%64%72%65%79%6f%72%73%74%2b%62%6c%6f%67%40%67%6d%61%69%6c%2e%63%6f%6d?subject=Comment: Clojure on Fennel part two: immutable.fnl optimizations&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Comment via email&lt;/a&gt;&lt;/div&gt;</description>
      <pubDate>Wed, 15 Apr 2026 05:11:00 +0300</pubDate>
    </item><item>
      <title>fnl-http - testing with fennel-test</title>
      <link>https://andreyor.st/posts/2024-08-15-fnl-http-testing-with-fennel-test/</link>
      <guid>https://andreyor.st/posts/2024-08-15-fnl-http-testing-with-fennel-test/</guid>
      <description>&lt;p&gt;&lt;a href=&#34;https://gitlab.com/andreyorst/fnl-http&#34; target=&#34;_blank&#34;&gt;fnl-http&lt;/a&gt; is my current passion project - I spend a lot of free time tinkering with it, and the last week was spent on testing and fixing bugs.&lt;/p&gt;
&lt;p&gt;As you may know, I made a testing framework, called &lt;a href=&#34;https://gitlab.com/andreyorst/fennel-test&#34; target=&#34;_blank&#34;&gt;fennel-test&lt;/a&gt;, which has a dedicated test runner, and a set of macros for writing tests.
I use it in all of my projects, and oftentimes, some project requires more features that are currently available in the framework, so I extend it as needed.
I think it&amp;rsquo;s a great way of designing something - you use it, and enhance it as you encounter new needs.&lt;/p&gt;
&lt;p&gt;I want programming to be exciting, so I spent a lot of time making my test report nice to look at.
Here&amp;rsquo;s how the report looks by default:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Test run at Tue Aug  6 23:20:41 2024, seed: 1722975641242
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(....)(......)(.)(.....)(.)(........)(...)(...)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The default reporter is called &lt;code&gt;dots&lt;/code&gt;.
Each test namespace is encoded with parentheses &lt;code&gt;()&lt;/code&gt;, each successful test is encoded as a dot &lt;code&gt;.&lt;/code&gt;, and failed tests are encoded as &lt;code&gt;F&lt;/code&gt;.
The runner randomizes the order of tests each time, so if we run it again, the result would be different:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Test run at Tue Aug  6 23:24:10 2024, seed: 1722975850235
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(......)(........)(.)(...)(...)(....)(.....)(.)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;However, this is all you get with a reporter like this.
If a test fails, information about the error is printed after all tests have finished, so you know what test did fail.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s another inbuilt reporter, called &lt;code&gt;namespaces&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Test run at Tue Aug  6 23:27:33 2024, seed: 1722976053226
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;test.http-parser-test: PASS
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;test.client-test: PASS
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;test.json-test: PASS
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;test.httpbin-test: PASS
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;test.body-test: PASS
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;test.headers-test: PASS
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;test.readers-test: PASS
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;test.url-test: PASS
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As you can see, it prints each namespace that is currently being tested, followed by the namespace-wide status.
If a test fails, you again see its error message, but only after all tests have finished, which isn&amp;rsquo;t ideal because tests in the &lt;code&gt;fnl-http&lt;/code&gt; project take about two minutes to complete.&lt;/p&gt;
&lt;p&gt;Fortunately enough, you can create reporters in the &lt;code&gt;.fennel-test&lt;/code&gt; configuration file!&lt;/p&gt;
&lt;h2 id=&#34;reporters&#34;&gt;Reporters&lt;/h2&gt;
&lt;p&gt;A reporter is just a table with a set of functions.
Each one is called when the respecting stage of testing is being run.
The methods are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ns-start&lt;/code&gt; - called when entering a new test namespace.&lt;/p&gt;
&lt;p&gt;Used to print the information about the namespace, such as its name.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ns-report&lt;/code&gt; - called when exiting a test namespace.&lt;/p&gt;
&lt;p&gt;Used to print the overall status of the namespace.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;test-start&lt;/code&gt; - called when running a single test from a namespace.&lt;/p&gt;
&lt;p&gt;Used to print the information about the test, such as the test&amp;rsquo;s name.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;test-report&lt;/code&gt; - called after the test is finished.&lt;/p&gt;
&lt;p&gt;Used to print the status of the test.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;stats-report&lt;/code&gt; - called after running all tests in all namespaces.&lt;/p&gt;
&lt;p&gt;Used to print overall statistics of tests - how many tests were executed, how many errors were encountered, were there any warnings, etc.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This was enough for me to implement other kind of reporters, like the one in the &lt;a href=&#34;https://gitlab.com/andreyorst/fennel-test#custom-reporters&#34; target=&#34;_blank&#34;&gt;project&amp;rsquo;s readme&lt;/a&gt;.
However, I wanted a bit more for this project - first, I wanted the report to be fun to look at, and second, I wanted some statistics.
And third, actually, I noticed the need to skip tests in case something is not available on the machine.&lt;/p&gt;
&lt;p&gt;After a bit of work, here&amp;rsquo;s what I&amp;rsquo;ve got:&lt;/p&gt;
&lt;figure class=&#34;invertable&#34;&gt;&lt;img src=&#34;https://andreyor.st/2024-08-15-fnl-http-testing-with-fennel-test/new-reporter.png&#34;/&gt;
&lt;/figure&gt;

&lt;p&gt;With this reporter, if a specific test fails, I can immediately see it, without waiting for all tests to pass.
In the end, you can see some statistics on the longest tests, and average test time of the namespace.
For example, the &lt;code&gt;test.httpbin-test&lt;/code&gt; namespace took 90 seconds to complete, but on average each test only takes 11 seconds.
I&amp;rsquo;m not entirely sure why this metric would be necessary, but I&amp;rsquo;ve seen it in another test runner, so I implemented it in my reporter.&lt;/p&gt;
&lt;p&gt;Now, here&amp;rsquo;s a problem - one of the tests takes a whopping 90 seconds.
I don&amp;rsquo;t want to run this test all the time, especially, when I&amp;rsquo;m fixing a bug found by some other test.
So I introduced a &lt;code&gt;skip-test&lt;/code&gt; function as a first step.&lt;/p&gt;
&lt;p&gt;Originally, this function had two modes - terminating and non-terminating, but I later removed the second mode.
The reason for that is the &lt;code&gt;with-open&lt;/code&gt; macro.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an example of one of the fixtures I use in the &lt;code&gt;fnl-http&lt;/code&gt; project:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-fennel&#34; data-lang=&#34;fennel&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;use-fixtures&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;:once&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; (&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;fn &lt;/span&gt;[&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;t&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   (&lt;span style=&#34;font-weight:bold&#34;&gt;with-open &lt;/span&gt;[&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;proc&lt;/span&gt; (&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;io.popen &lt;/span&gt;(&lt;span style=&#34;font-weight:bold&#34;&gt;.. &lt;/span&gt;&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;podman run  -p &amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;port&lt;/span&gt; &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;:80 kennethreitz/httpbin &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp; echo $!&amp;#34;&lt;/span&gt;))]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     (&lt;span style=&#34;font-weight:bold&#34;&gt;let &lt;/span&gt;[&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;pid&lt;/span&gt; (&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;proc&lt;/span&gt;&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;:read&lt;/span&gt; &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;:*l&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;attempts&lt;/span&gt; 10]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       (&lt;span style=&#34;font-weight:bold&#34;&gt;if &lt;/span&gt;(&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;wait-for-server&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;attempts&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           (&lt;span style=&#34;font-weight:bold&#34;&gt;do &lt;/span&gt;(&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;t&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;               (&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;kill&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;pid&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           (&lt;span style=&#34;font-weight:bold&#34;&gt;do &lt;/span&gt;(&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;kill&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;pid&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;               (&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;skip-test&lt;/span&gt; (&lt;span style=&#34;font-weight:bold&#34;&gt;.. &lt;/span&gt;&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;coudln&amp;#39;t connect to httpbin server after &amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;attempts&lt;/span&gt; &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34; attempts&amp;#34;&lt;/span&gt;))))))))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;with-open&lt;/code&gt; macro ensures that the resource is closed no matter what.
Closing the process, however, doesn&amp;rsquo;t kill the program running in the background, so I had to get its PID with sketchy &lt;code&gt;echo $!&lt;/code&gt; part and kill it later.
Killing the process doesn&amp;rsquo;t release the file descriptor created by &lt;code&gt;popen&lt;/code&gt;, so &lt;code&gt;with-open&lt;/code&gt; is only responsible for releasing the file handle here.&lt;/p&gt;
&lt;h3 id=&#34;a-sudden-bug&#34;&gt;A sudden bug&lt;/h3&gt;
&lt;p&gt;What &lt;code&gt;with-open&lt;/code&gt; also does is catching, and re-throwing errors.
It&amp;rsquo;s something akin to trying with resources in Java, and other languages that support this kind of abstraction.
You can think about it as &lt;code&gt;try&lt;/code&gt;/&lt;code&gt;finally&lt;/code&gt; without the &lt;code&gt;catch&lt;/code&gt; part.&lt;/p&gt;
&lt;p&gt;Lua, however, doesn&amp;rsquo;t really have well-defined exceptions.
Instead, functions usually throw string messages about the error and that&amp;rsquo;s it.
Sometimes, however, it&amp;rsquo;s useful to throw a table instead.
Tables are like objects, so you can embed some additional information in those, making it easier to match a particular error.
It&amp;rsquo;s better to convert this table to a string if it ever reaches the user uncaught though.&lt;/p&gt;
&lt;p&gt;What makes things harder is that in Lua it&amp;rsquo;s not possible to re-throw an error without losing its previous stack trace.
While it is a common practice to use &lt;code&gt;pcall&lt;/code&gt; to catch any errors and to re-throw them if you can&amp;rsquo;t do anything about the error, this is what causes losing the precious stack trace.
More precisely, if the error message comes with the stack trace, and you would just re-throw it with &lt;code&gt;error&lt;/code&gt;, you wouldn&amp;rsquo;t add your stack trace to the trace.
Sounds convoluted, yeah?&lt;/p&gt;
&lt;p&gt;So Lua gives you another option: &lt;code&gt;xpcall&lt;/code&gt;.
The &lt;a href=&#34;https://www.lua.org/manual/5.4/manual.html#pdf-xpcall&#34; target=&#34;_blank&#34;&gt;Lua manual&lt;/a&gt; doesn&amp;rsquo;t have a thorough explanation of it though:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;xpcall (f, msgh [, arg1, ···])&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This function is similar to &lt;code&gt;pcall&lt;/code&gt;, except that it sets a new message handler &lt;code&gt;msgh&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It works the same as &lt;code&gt;pcall&lt;/code&gt;, except, it accepts an additional argument - a function that would append a stack trace to the error.
Usually it&amp;rsquo;s the &lt;code&gt;debug.traceback&lt;/code&gt; &lt;a href=&#34;https://www.lua.org/manual/5.4/manual.html#pdf-debug.traceback&#34; target=&#34;_blank&#34;&gt;function&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;debug.traceback ([thread,] [message [, level]])&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;If message is present but is neither a string nor &lt;strong&gt;nil&lt;/strong&gt;, this function returns message without further processing.
Otherwise, it returns a string with a traceback of the call stack.
The optional message string is appended at the beginning of the traceback.
An optional &lt;code&gt;level&lt;/code&gt; number tells at which level to start the traceback (default is 1, the function calling &lt;code&gt;traceback&lt;/code&gt;).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Knowing all that, let&amp;rsquo;s look at how I&amp;rsquo;ve implemented the &lt;code&gt;skip-test&lt;/code&gt; function:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-fennel&#34; data-lang=&#34;fennel&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;local &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;Skip&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  (&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;setmetatable&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   {}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   {&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;:__tostring&lt;/span&gt; #&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;:Skip&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;:__fennelview&lt;/span&gt; #&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;:Skip&lt;/span&gt;}))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;fn &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;skip-test&lt;/span&gt; [&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;reason&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;Calling this function inside a test or a fixture will stop the test
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;early and mark it as skipped. The optional `reason` argument is a
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;message to display in the log if the reporter is configured to do so.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  (&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;error &lt;/span&gt;[&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;Skip&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;reason&lt;/span&gt;]))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Right, &lt;code&gt;skip-test&lt;/code&gt; throws an array with a special table object, defined as a singleton, so the test runner could distinguish between ordinary test errors and skipped tests.
In theory, everything should have worked, but for some reason, some of the tests were skipped correctly, but others didn&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;The culprit?
It&amp;rsquo;s &lt;code&gt;with-open&lt;/code&gt;, obviously, otherwise, why would I take such a tangent about it?&lt;/p&gt;
&lt;p&gt;Internally, &lt;code&gt;with-open&lt;/code&gt; uses the &lt;code&gt;xpcall&lt;/code&gt; function to do its job.
And theoretically, it should work just fine, however, it doesn&amp;rsquo;t use &lt;code&gt;debug.traceback&lt;/code&gt; if Fennel&amp;rsquo;s own &lt;code&gt;compiler.traceback&lt;/code&gt; is available.&lt;/p&gt;
&lt;p&gt;Originally, I thought that&amp;rsquo;s just how it is, and implemented two modes in &lt;code&gt;skip-test&lt;/code&gt;.
After a bit of digging and consulting the manual, I found out that &lt;code&gt;compiler.traceback&lt;/code&gt; mistakenly converted &lt;strong&gt;every&lt;/strong&gt; message to a string.
&lt;a href=&#34;https://lists.sr.ht/~technomancy/fennel/patches/54416&#34; target=&#34;_blank&#34;&gt;Fixing that&lt;/a&gt; made the &lt;code&gt;skip-test&lt;/code&gt; function work in all cases.&lt;/p&gt;
&lt;h3 id=&#34;another-sudden-bug&#34;&gt;Another sudden bug!&lt;/h3&gt;
&lt;p&gt;After the patch landed, and I updated Fennel to the newest commit, something else broke.
Now, the JSON parsing test no longer succeeded, complaining that some number has no integer representation.
The number in question is &lt;code&gt;23456789012000000000000000000000000000000000000000000000000000000000000000000&lt;/code&gt; or, as it is written in the JSON test file that I use, &lt;code&gt;2.3456789012e+76&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Before the update, everything worked fine, so I started digging again.
Turns out, recently, the way numbers are compiled by Fennel has changed:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span style=&#34;font-weight:bold;display:flex;&#34;&gt;&lt;span&gt;@@ -534,8 +534,9 @@ (fn compile-sym
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;color:gray;display:flex;&#34;&gt;&lt;span&gt; ;; We do gsub transformation because some locales use , for
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;color:gray;display:flex;&#34;&gt;&lt;span&gt; ;; decimal separators, which will not be accepted by Lua.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;color:gray;display:flex;&#34;&gt;&lt;span&gt; (fn serialize-number [n]
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;diff-removed&#34; style=&#34;display:flex;&#34;&gt;&lt;span&gt;-  (pick-values 1 (-&amp;gt; (tostring n)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;diff-removed&#34; style=&#34;display:flex;&#34;&gt;&lt;span&gt;-                     (string.gsub &#34;,&#34; &#34;.&#34;))))
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;diff-added&#34; style=&#34;display:flex;&#34;&gt;&lt;span&gt;+  (if (= (math.floor n) n)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;diff-added&#34; style=&#34;display:flex;&#34;&gt;&lt;span&gt;+      (string.format &#34;%d&#34; n)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;diff-added&#34; style=&#34;display:flex;&#34;&gt;&lt;span&gt;+      (string.gsub (tostring n) &#34;,&#34; &#34;.&#34;)))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And now it is failing, because indeed, you can&amp;rsquo;t convert &lt;code&gt;2.3456789012e+76&lt;/code&gt; to a string using &lt;code&gt;%d&lt;/code&gt;.
Except, you can if you&amp;rsquo;re using LuaJIT - it happily converts this number to &lt;code&gt;&amp;quot;-9223372036854775808&amp;quot;&lt;/code&gt;, which is even worse.&lt;/p&gt;
&lt;p&gt;After a bit of searching, I found that the safest way to format &lt;strong&gt;integers&lt;/strong&gt; as stings is by using &lt;code&gt;&amp;quot;%.f&lt;/code&gt; format specifier.
Strangely enough, the Lua manual doesn&amp;rsquo;t have any information on most number formatting patterns.
A tiny bit of information was found on the lua-users.org &lt;a href=&#34;https://www.lua-users.org/wiki/IntegerDomain&#34; target=&#34;_blank&#34;&gt;page&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;To make sure integer numbers are properly converted into string (without scientific notation), rather than using &lt;code&gt;tostring(x)&lt;/code&gt;, use &lt;code&gt;format.string(&amp;quot;%.0f&amp;quot;,x)&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I don&amp;rsquo;t really get why &lt;code&gt;%.f&lt;/code&gt; would format a number as an integer given that &lt;code&gt;%f&lt;/code&gt; formats floats.
And, even though it works the same way in C, I have never seen this in my life.
But OK, we could just replace &lt;code&gt;%d&lt;/code&gt; with &lt;code&gt;%.f&lt;/code&gt;, and be done with that, however, the number above is then would appear as &lt;code&gt;&amp;quot;23456789012000000697746671432670411048021854502345623879537911769302235611136&amp;quot;&lt;/code&gt;.
I get it, it&amp;rsquo;s a big number, and Lua has limited precision for integers, but the main problem is that this number is much harder to read than its e-notation brother &lt;code&gt;2.3456789012e+76&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So I settled on finding a way to fix this bug so that both PUC Lua and LuaJIT would format this number in the same way.
The result is a &lt;a href=&#34;https://lists.sr.ht/~technomancy/fennel/patches/54443&#34; target=&#34;_blank&#34;&gt;patch&lt;/a&gt;, that changes the compiler and pretty-printer parts of the language.
The function that does that, looks horrifying:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-fennel&#34; data-lang=&#34;fennel&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;fn &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;serialize-number&lt;/span&gt; [&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;n&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  (&lt;span style=&#34;font-weight:bold&#34;&gt;let &lt;/span&gt;[&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;val&lt;/span&gt; (&lt;span style=&#34;font-weight:bold&#34;&gt;if &lt;/span&gt;(&lt;span style=&#34;font-weight:bold&#34;&gt;= &lt;/span&gt;(&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;math.floor &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;n&lt;/span&gt;) &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;n&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                (&lt;span style=&#34;font-weight:bold&#34;&gt;let &lt;/span&gt;[&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;s1&lt;/span&gt; (&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;string.format &lt;/span&gt;&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;%.f&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;n&lt;/span&gt;)]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                  (&lt;span style=&#34;font-weight:bold&#34;&gt;if &lt;/span&gt;(&lt;span style=&#34;font-weight:bold&#34;&gt;= &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;s1&lt;/span&gt; (&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;tostring &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;n&lt;/span&gt;)) &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;s1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                      (&lt;span style=&#34;font-weight:bold&#34;&gt;or &lt;/span&gt;(&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;faccumulate&lt;/span&gt; [&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;s&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;nil&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;i&lt;/span&gt; 0 99 &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;:until&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;s&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                            (&lt;span style=&#34;font-weight:bold&#34;&gt;let &lt;/span&gt;[&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;s&lt;/span&gt; (&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;string.format &lt;/span&gt;(&lt;span style=&#34;font-weight:bold&#34;&gt;.. &lt;/span&gt;&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;%.&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;i&lt;/span&gt; &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;e&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;n&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                  &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;n*&lt;/span&gt; (&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;tonumber &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;s&lt;/span&gt;)]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                              (&lt;span style=&#34;font-weight:bold&#34;&gt;when &lt;/span&gt;(&lt;span style=&#34;font-weight:bold&#34;&gt;= &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;n&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;n*&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                (&lt;span style=&#34;font-weight:bold&#34;&gt;let &lt;/span&gt;[&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;exp&lt;/span&gt; (&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;s&lt;/span&gt;&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;:match&lt;/span&gt; &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;e%+?(%d+)$&amp;#34;&lt;/span&gt;)]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;;; Lua stops transforming numbers from&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;;; e-notation to integers at e+14&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                  (&lt;span style=&#34;font-weight:bold&#34;&gt;if &lt;/span&gt;(&lt;span style=&#34;font-weight:bold&#34;&gt;and &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;exp&lt;/span&gt; (&lt;span style=&#34;font-weight:bold&#34;&gt;&amp;gt; &lt;/span&gt;(&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;tonumber &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;exp&lt;/span&gt;) 14))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                      &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                      &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;s1&lt;/span&gt;)))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                              &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;s1&lt;/span&gt;))))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                  (&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;tostring &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;n&lt;/span&gt;))]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        (&lt;span style=&#34;font-weight:bold&#34;&gt;pick-values &lt;/span&gt;1 (&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;string.gsub &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;val&lt;/span&gt; &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;,&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;.&amp;#34;&lt;/span&gt;))))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We start by checking if &lt;code&gt;(tostring n)&lt;/code&gt; and &lt;code&gt;(string.format &amp;quot;%.f&amp;quot; n)&lt;/code&gt; give the same string.
If they are, we just return the string.
If not, we try the scientific notation for each power from &lt;code&gt;0&lt;/code&gt; to &lt;code&gt;99&lt;/code&gt;.
I probably should&amp;rsquo;ve tested for more powers than that, but it&amp;rsquo;s OK for now.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re converting the number to a string using the e-notation and parse it back, comparing it with the original number.
What this does, is that if the power is less than the original, we would lose precision, and comparison would fail.
If we can&amp;rsquo;t find the exponent up to 99, we bail out and return the &lt;code&gt;%.f&lt;/code&gt; representation.
Otherwise, if the numbers match, we check if the exponent is greater than &lt;code&gt;14&lt;/code&gt; as it is the exponent where PUC Lua starts using the e-notation.
Otherwise, we again return the &lt;code&gt;%.f&lt;/code&gt; representation.&lt;/p&gt;
&lt;p&gt;Phew!&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://www.youtube.com/watch?v=aM8YOBvq-jc&#34; target=&#34;_blank&#34;&gt;But I&amp;rsquo;m not done yet!&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s try to compile a really huge number:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-fennel&#34; data-lang=&#34;fennel&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;&amp;gt;&amp;gt;&lt;/span&gt; 1&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;e+99999&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;What?&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s do it in an expression then, maybe that&amp;rsquo;s just the pretty printer freaking out:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-fennel&#34; data-lang=&#34;fennel&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;&amp;gt;&amp;gt;&lt;/span&gt; (&lt;span style=&#34;font-weight:bold&#34;&gt;+ &lt;/span&gt;1&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;e+309&lt;/span&gt; 1)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;attempt&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;to&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;perform&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;arithmetic&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;on&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;a&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;nil&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;value&lt;/span&gt; (&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;global &lt;/span&gt;&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#39;inf&lt;/span&gt;&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;del&gt;&lt;b&gt;What?&lt;/b&gt;&lt;/del&gt; I, of course, jest, but the reason this happens is pretty peculiar.&lt;/p&gt;
&lt;p&gt;You see - when we do &lt;code&gt;(tostring &amp;quot;%.f&amp;quot; n)&lt;/code&gt; on a huge number like &lt;code&gt;1e+309&lt;/code&gt; it returns the string &lt;code&gt;&amp;quot;inf&amp;quot;&lt;/code&gt;.
The compiler then happily includes it in the output Lua code, which is then run by the Lua VM.
In Lua, however, &lt;code&gt;inf&lt;/code&gt; is not a reserved identifier or a number, so it treats &lt;code&gt;inf&lt;/code&gt; like a variable name.
The &lt;code&gt;inf&lt;/code&gt; variable is unbound, hence the &lt;code&gt;nil&lt;/code&gt; in the first example, and an error in the second.&lt;/p&gt;
&lt;p&gt;So we need to account for that:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-fennel&#34; data-lang=&#34;fennel&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;fn &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;serialize-number&lt;/span&gt; [&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;n&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  (&lt;span style=&#34;font-weight:bold&#34;&gt;let &lt;/span&gt;[&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;val&lt;/span&gt; (&lt;span style=&#34;font-weight:bold&#34;&gt;if &lt;/span&gt;(&lt;span style=&#34;font-weight:bold&#34;&gt;= &lt;/span&gt;(&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;math.floor &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;n&lt;/span&gt;) &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;n&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                (&lt;span style=&#34;font-weight:bold&#34;&gt;let &lt;/span&gt;[&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;s1&lt;/span&gt; (&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;string.format &lt;/span&gt;&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;%.f&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;n&lt;/span&gt;)]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                  (&lt;span style=&#34;font-weight:bold&#34;&gt;if &lt;/span&gt;(&lt;span style=&#34;font-weight:bold&#34;&gt;= &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;s1&lt;/span&gt; &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;inf&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;(1/0)&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;; portable inf&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                      (&lt;span style=&#34;font-weight:bold&#34;&gt;= &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;s1&lt;/span&gt; &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;-inf&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;(-1/0)&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                      (&lt;span style=&#34;font-weight:bold&#34;&gt;= &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;s1&lt;/span&gt; (&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;tostring &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;n&lt;/span&gt;)) &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;s1&lt;/span&gt; &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;; no precision loss&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                      (&lt;span style=&#34;font-weight:bold&#34;&gt;or &lt;/span&gt;(&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;faccumulate&lt;/span&gt; [&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;s&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                        &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;i&lt;/span&gt; 0 308 &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;; beyond 308 every number turns to inf&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                        &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;:until&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;s&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                            (&lt;span style=&#34;font-weight:bold&#34;&gt;let &lt;/span&gt;[&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;s&lt;/span&gt; (&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;string.format &lt;/span&gt;(&lt;span style=&#34;font-weight:bold&#34;&gt;.. &lt;/span&gt;&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;%.&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;i&lt;/span&gt; &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;e&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;n&lt;/span&gt;)]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                              (&lt;span style=&#34;font-weight:bold&#34;&gt;when &lt;/span&gt;(&lt;span style=&#34;font-weight:bold&#34;&gt;= &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;n&lt;/span&gt; (&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;tonumber &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;s&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                (&lt;span style=&#34;font-weight:bold&#34;&gt;let &lt;/span&gt;[&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;exp&lt;/span&gt; (&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;s&lt;/span&gt;&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;:match&lt;/span&gt; &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;e%+?(%d+)$&amp;#34;&lt;/span&gt;)]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;;; Lua keeps numbers in standard notation up to e+14&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                  (&lt;span style=&#34;font-weight:bold&#34;&gt;if &lt;/span&gt;(&lt;span style=&#34;font-weight:bold&#34;&gt;and &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;exp&lt;/span&gt; (&lt;span style=&#34;font-weight:bold&#34;&gt;&amp;gt; &lt;/span&gt;(&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;tonumber &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;exp&lt;/span&gt;) 14))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                      &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                      &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;s1&lt;/span&gt;)))))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                          &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;s1&lt;/span&gt;)))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                (&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;tostring &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;n&lt;/span&gt;))]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    (&lt;span style=&#34;font-weight:bold&#34;&gt;pick-values &lt;/span&gt;1 (&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;string.gsub &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;val&lt;/span&gt; &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;,&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;.&amp;#34;&lt;/span&gt;))))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I also brought the exponent limit up to the last number representable in this notation without becoming &lt;code&gt;inf&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Now we can go back to testing!&lt;/p&gt;
&lt;h2 id=&#34;counting-skipped-tests-and-assertions&#34;&gt;Counting skipped tests and assertions&lt;/h2&gt;
&lt;p&gt;The other thing I added to &lt;code&gt;fennel-test&lt;/code&gt; was more counters.
Counting skipped tests required some tinkering, like counting tests in the whole namespace if the tests were skipped from the &lt;code&gt;:once&lt;/code&gt; fixture.&lt;/p&gt;
&lt;p&gt;Counting assertions was even harder, because tests in &lt;code&gt;fennel-test&lt;/code&gt; are implemented through macros, and assertions are macros too.
I had to inject some runner state, conditionally, and modify it at runtime from each assertion.
A bit shady but it works.&lt;/p&gt;
&lt;p&gt;What is much more shady is the coverage reports for this project.&lt;/p&gt;
&lt;h2 id=&#34;coverage-report&#34;&gt;Coverage report&lt;/h2&gt;
&lt;p&gt;I have a firm belief that &lt;a href=&#34;https://andreyor.st/posts/2022-03-29-100-is-the-only-acceptable-test-coverage/&#34;&gt;100% is the only acceptable coverage&lt;/a&gt;.
However, and this is a big &lt;span style=&#34;font-size: 1.33em&#34;&gt;however&lt;/span&gt;, it&amp;rsquo;s important that such coverage comes from public API tests only.
Achieving 100% coverage this way means that your program doesn&amp;rsquo;t have any dead code and that your public API test at least touches all of our code at least once.
It doesn&amp;rsquo;t speak about test quality, of course, and it doesn&amp;rsquo;t guarantee that the program is bug-free, but those other points alone make a huge difference.&lt;/p&gt;
&lt;p&gt;Lua has one and only &lt;a href=&#34;https://lunarmodules.github.io/luacov/&#34; target=&#34;_blank&#34;&gt;luacov&lt;/a&gt; that you can use to test your code.
I&amp;rsquo;ve used it in some other Fennel projects in the past, but it comes with some nuances you have to deal with.&lt;/p&gt;
&lt;p&gt;First, luacov itself is imprecise.
In the post about 100% coverage I talk about it in more detail, but basically, I often see situations where the side effects happened, but the coverage report showed code responsible for the side effects as untouched.
Other times, the coverage percentage is bigger than its actual value due to code formatting.&lt;/p&gt;
&lt;p&gt;With the &lt;code&gt;fnl-http&lt;/code&gt; project I hit a different problem.
Two, actually.&lt;/p&gt;
&lt;p&gt;The first one is related to the &lt;code&gt;--correlate&lt;/code&gt; option of the Fennel compiler.
I had an idea, that if I used &lt;code&gt;--correlate&lt;/code&gt; I could substitute Lua sources used to produce the coverage with Fennel sources.
And since the line numbers are matched by specifying the &lt;code&gt;--correlate&lt;/code&gt; option, the coverage report would be so much easier to read.&lt;/p&gt;
&lt;p&gt;Didn&amp;rsquo;t work out.&lt;/p&gt;
&lt;p&gt;Unfortunately, &lt;code&gt;--coverage&lt;/code&gt; is imprecise.
Many parts of Fennel AST don&amp;rsquo;t contain any line information.
Here&amp;rsquo;s an example:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;
&lt;table style=&#34;border-spacing:0;padding:0;margin:0;border:0;&#34;&gt;&lt;tr&gt;&lt;td style=&#34;vertical-align:top;padding:0;margin:0;border:0;&#34;&gt;
&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code&gt;&lt;span style=&#34;white-space:pre;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;1
&lt;/span&gt;&lt;span style=&#34;white-space:pre;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;2
&lt;/span&gt;&lt;span style=&#34;white-space:pre;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;3
&lt;/span&gt;&lt;span style=&#34;white-space:pre;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;4
&lt;/span&gt;&lt;span style=&#34;white-space:pre;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;5
&lt;/span&gt;&lt;span style=&#34;white-space:pre;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style=&#34;vertical-align:top;padding:0;margin:0;border:0;;width:100%&#34;&gt;
&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-fennel&#34; data-lang=&#34;fennel&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;local &lt;/span&gt;{&lt;span style=&#34;font-weight:bold&#34;&gt;: &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;a&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-weight:bold&#34;&gt;: &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;b&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  (&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;require &lt;/span&gt;&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;:c&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{&lt;span style=&#34;font-weight:bold&#34;&gt;: &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;a&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;font-weight:bold&#34;&gt;: &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;b&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;And here&amp;rsquo;s the corresponding Lua code:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;
&lt;table style=&#34;border-spacing:0;padding:0;margin:0;border:0;&#34;&gt;&lt;tr&gt;&lt;td style=&#34;vertical-align:top;padding:0;margin:0;border:0;&#34;&gt;
&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code&gt;&lt;span style=&#34;white-space:pre;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;1
&lt;/span&gt;&lt;span style=&#34;white-space:pre;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;2
&lt;/span&gt;&lt;span style=&#34;white-space:pre;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;3
&lt;/span&gt;&lt;span style=&#34;white-space:pre;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;4
&lt;/span&gt;&lt;span style=&#34;white-space:pre;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;5
&lt;/span&gt;&lt;span style=&#34;white-space:pre;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style=&#34;vertical-align:top;padding:0;margin:0;border:0;;width:100%&#34;&gt;
&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;local&lt;/span&gt; _local_1_ = require(&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;c&amp;#34;&lt;/span&gt;) &lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;local&lt;/span&gt; a = _local_1_[&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;a&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;local&lt;/span&gt; b = _local_1_[&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;b&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;-- empty line&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;-- empty line&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold&#34;&gt;return&lt;/span&gt; {a = a, b = b}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;-- empty line&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;As you can see, the overall amount of lines is the same, but many lines that are not empty in the Fennel example are empty in Lua.
It&amp;rsquo;s not a problem, as long as you&amp;rsquo;re measuring the coverage and viewing the report with Lua, but if we replace the Lua code with its Fennel counterpart, the report becomes invalid.
I guess, when the report is constructed, luacov checks if the line is empty or not, and if it isn&amp;rsquo;t, and there were no hits registered, it counts it as a miss.
I&amp;rsquo;m using &lt;code&gt;luacov-console&lt;/code&gt; to view the report, maybe it&amp;rsquo;s a bug from there, I don&amp;rsquo;t know.&lt;/p&gt;
&lt;p&gt;The only way to fix this is to write the code accounting for these nuances of &lt;code&gt;--correlate&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-fennel&#34; data-lang=&#34;fennel&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;local &lt;/span&gt;{&lt;span style=&#34;font-weight:bold&#34;&gt;: &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;a&lt;/span&gt;                &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;; local _local_1_ = require(&amp;#34;c&amp;#34;) local a = _local_1_[&amp;#34;a&amp;#34;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-weight:bold&#34;&gt;: &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;b&lt;/span&gt;} (&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;require &lt;/span&gt;&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;:c&lt;/span&gt;)) &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;; local b = _local_1_[&amp;#34;b&amp;#34;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                           &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;; &amp;lt;empty line&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{&lt;span style=&#34;font-weight:bold&#34;&gt;: &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;a&lt;/span&gt; &lt;span style=&#34;font-weight:bold&#34;&gt;: &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;b&lt;/span&gt;}                  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;; return {a = a, b = b}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There are more examples like that in the project&amp;rsquo;s code.&lt;/p&gt;
&lt;p&gt;The other problem is more peculiar.
Luacov uses &lt;code&gt;debug.sethook&lt;/code&gt; to build its report.
And I use the &lt;code&gt;async.fnl&lt;/code&gt; library in this project, which also uses &lt;code&gt;debug.sethook&lt;/code&gt; to schedule tasks.
So every time anything happens asynchronously, and trust me, there&amp;rsquo;s a lot of that in &lt;code&gt;fnl-http&lt;/code&gt;, the Luacov report is no longer properly calculated.&lt;/p&gt;
&lt;p&gt;So, Luacov has a lot of problems, but I understand why it is like that.
You can&amp;rsquo;t really instrument the Lua code in some other way.&lt;/p&gt;
&lt;p&gt;Fennel, on the other hand, isn&amp;rsquo;t like that.
Being a language where everything is an expression, we can probably make some compiler plugin that injects some side-effecting code that writes the report for every expression in the code.
It would be difficult to do with things like tables and destructuring, but it should be doable.
Maybe I&amp;rsquo;ll try to tackle this problem in the future, who knows?&lt;/p&gt;
&lt;p&gt;Anyway, that&amp;rsquo;s all from me for now!
As usual, you can check out the &lt;a href=&#34;https://gitlab.com/andreyorst/fnl-http&#34; target=&#34;_blank&#34;&gt;project&lt;/a&gt;, which contains all of the stuff mentioned above regarding &lt;code&gt;fennel-test&lt;/code&gt; and &lt;code&gt;luacov&lt;/code&gt;.
Thanks for reading!&lt;/p&gt;
&lt;div class=&#34;comment-link&#34;&gt; &lt;a href=&#34;mailto:%61%6e%64%72%65%79%6f%72%73%74%2b%62%6c%6f%67%40%67%6d%61%69%6c%2e%63%6f%6d?subject=Comment: fnl-http - testing with fennel-test&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Comment via email&lt;/a&gt;&lt;/div&gt;</description>
      <pubDate>Thu, 15 Aug 2024 23:01:00 +0300</pubDate>
    </item><item>
      <title>100% is the only acceptable test coverage</title>
      <link>https://andreyor.st/posts/2022-03-29-100-is-the-only-acceptable-test-coverage/</link>
      <guid>https://andreyor.st/posts/2022-03-29-100-is-the-only-acceptable-test-coverage/</guid>
      <description>&lt;p&gt;…kinda.&lt;/p&gt;
&lt;p&gt;I think that the only acceptable test coverage percentage is &lt;em&gt;about 100%&lt;/em&gt;.
And in this post, I&amp;rsquo;ll try to explain why I choose to believe it.&lt;/p&gt;
&lt;h2 id=&#34;why-test-coverage-matters&#34;&gt;Why test coverage matters&lt;/h2&gt;
&lt;p&gt;I have a lot of projects of various sizes.
In most cases, these projects are libraries, that I host publicly, and some tools I&amp;rsquo;ve written for myself only.
What&amp;rsquo;s common about these projects is that no matter if it&amp;rsquo;s a public project or my personal one, I&amp;rsquo;m trying to test those as well as I can.&lt;/p&gt;
&lt;p&gt;But this raises some important questions, one of which is how do we measure the quality of our tests?
A rather obvious answer to that question is test coverage.
And I can only say that this is the stupidest metric to measure test quality because coverage doesn&amp;rsquo;t actually guarantee that tests check all various corner cases data-wise.&lt;/p&gt;
&lt;p&gt;Test quality is a bit different topic, and while code coverage is not what measures it, it is still important, and in my opinion, the reasonable percentage is 100% or something really close to it.
I&amp;rsquo;ll explain why in a moment, but before that, I&amp;rsquo;ll address a question that probably has appeared in your head: &amp;ldquo;why go as high as 100%?&amp;rdquo;.
And that&amp;rsquo;s a valid question.&lt;/p&gt;
&lt;p&gt;Recently, I&amp;rsquo;ve talked with some developers who use different languages and was kinda surprised to hear that on average, the acceptable test coverage is ~60%.
Some say that writing unit tests for functions that are inherently simple is meaningless, as those can be verified fully at the stage of writing the function itself.
Others say that 60% is just enough to cover the public API and catch most of the bugs that the end-user may experience.
Some even say that test coverage doesn&amp;rsquo;t matter at all, and only integration coverage actually does, but it&amp;rsquo;s hard to count, so nobody really cares.
But even they agreed when I asked them if 60% would be enough for them.&lt;/p&gt;
&lt;p&gt;I disagree strongly.&lt;/p&gt;
&lt;p&gt;You may have heard of the 80/20 rule, also known as &lt;a href=&#34;https://en.wikipedia.org/wiki/Pareto_principle&#34; target=&#34;_blank&#34;&gt;Pareto principle&lt;/a&gt;.
The Wikipedia article I&amp;rsquo;ve linked has a section related to computer science, that states that:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In computer science, the Pareto principle can be applied to optimization efforts.
For example, Microsoft noted that by fixing the top 20% of the most-reported bugs, 80% of the related errors and crashes in a given system would be eliminated.
Lowell Arthur expressed that &amp;ldquo;20% of the code has 80% of the errors.
Find them, fix them!&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And after a while of writing tests and measuring code coverage for my projects, I noticed something similar to that.
&lt;strong&gt;The last 20% of code coverage held 80% of bugs of the code.&lt;/strong&gt;
And the interesting part was, that no matter how good my tests were, it didn&amp;rsquo;t matter, as they weren&amp;rsquo;t touching the parts of the code that actually had unnoticed bugs.
So whenever I&amp;rsquo;ve stopped at code coverage of 80% saying to myself that this is good enough, I was lying to myself, as it clearly wasn&amp;rsquo;t.&lt;/p&gt;
&lt;h2 id=&#34;what-code-coverage-is-about&#34;&gt;What code coverage is about&lt;/h2&gt;
&lt;p&gt;So, now let me address some misconceptions about code coverage.&lt;/p&gt;
&lt;p&gt;Foremost, test coverage is &lt;strong&gt;not about&lt;/strong&gt; covering &lt;strong&gt;every function&lt;/strong&gt; in your project with a unit test.
This is pointless extra work that will be negated with the first tiny code refactoring.
Because of that, all these tests will become obsolete, and new tests are likely won&amp;rsquo;t be ever written, because why bother, if they&amp;rsquo;ll become irrelevant with the next refactoring?
And functions are really simple, why even test those?&lt;/p&gt;
&lt;p&gt;Second, for those who love numbers, 60% coverage in a project with just 2k lines of code (LOC) means, that 800 lines were never even touched by the existing tests.
In a project with 10k LOC, it&amp;rsquo;s already 4k lines.
Not only you can&amp;rsquo;t reason about correctness, but you also can&amp;rsquo;t tell what&amp;rsquo;s actually used in your project and what&amp;rsquo;s not!&lt;/p&gt;
&lt;p&gt;And &lt;strong&gt;that&amp;rsquo;s&lt;/strong&gt; the point of code coverage - telling you what parts of your application/library are not reachable from the public API that you always must test.
You &lt;strong&gt;don&amp;rsquo;t need&lt;/strong&gt; to write unit tests for anything but the public API in almost all cases!
Yes, it&amp;rsquo;s good to write tests that check complex functions, which do some data transformation for example, with a lot of different test inputs - I&amp;rsquo;m not against that at all.
However, if you write a unit test for a private function, think of it for a moment, maybe you&amp;rsquo;re doing something wrong.&lt;/p&gt;
&lt;p&gt;As of this moment, I&amp;rsquo;m mainly using two languages - Fennel and Clojure, and the part about testing private functions is what makes these languages a bit different for me when I&amp;rsquo;m reasoning about code and its public API.
The difference is that in Fennel there&amp;rsquo;s no way of accessing a private function of a module, and hence there&amp;rsquo;s no way to test it directly.
In Clojure, there is, and when I see, or even have to use &lt;code&gt;(#&#39;some-ns/some-private-fn)&lt;/code&gt;&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; in a test, it&amp;rsquo;s a huge red flag for me, that something is not right in the application code itself.
Fennel (actually Lua) doesn&amp;rsquo;t have this problem, because if something is declared as &lt;code&gt;local&lt;/code&gt; in a script file and not exported from it in a table, that&amp;rsquo;s that - you can&amp;rsquo;t have it.&lt;/p&gt;
&lt;p&gt;So when writing tests in Fennel or Lua, your only option is to test exported functions only, which is a kind of your public API.
And if you wrote tests, and see that all lines are covered, all expressions are visited, and you still can&amp;rsquo;t reach some private functions - well, that&amp;rsquo;s a clear indication of a dead code.
Dead code is bad because it rots even if you don&amp;rsquo;t touch it.
Even more - if you actually go and write unit tests for such dead code, you&amp;rsquo;ll make the situation even worse for yourself in the long run.
After a certain period, these tests may be the reason you can&amp;rsquo;t move forward, because you&amp;rsquo;ll be afraid to change or even remove a code that &lt;em&gt;appears&lt;/em&gt; to be unused - but you can&amp;rsquo;t be sure, because there are tests for this code.&lt;/p&gt;
&lt;p&gt;Now, I know what you can say to this - modern IDEs can detect unused code by using static analysis.
This is a great feature that helps to deal with this problem to some degree.
First, some IDEs detect that code is used in tests, and that&amp;rsquo;s enough for them to consider code not to be dead, which isn&amp;rsquo;t actually true.
It is an important feature, but not a silver bullet, and not all languages have it.
Yes, in most cases it helps you see that some code is indeed unused, but that&amp;rsquo;s the only thing it does.
Test coverage also shows that, plus provides additional correctness guarantees of your code because of tests that you&amp;rsquo;ve written.&lt;/p&gt;
&lt;h2 id=&#34;why-100-is-not-always-possible&#34;&gt;Why 100% is not always possible&lt;/h2&gt;
&lt;p&gt;I hope you now see why I think why code coverage is very important.
But percentage doesn&amp;rsquo;t exactly say to you what is left uncovered, so all code coverage tools provide some kind of report, usually in a color-coded form.
Some coverage tools build interactive web pages that allow easy project navigation, and some just print results to &lt;code&gt;stdout&lt;/code&gt; using ANSI color codes.
Both are equally helpful because you see the results of your work.&lt;/p&gt;
&lt;p&gt;However, unfortunately, it&amp;rsquo;s not as simple as that.
If it had been, I guess the average coverage percentage &lt;em&gt;expectage&lt;/em&gt; would be higher than 60%.
So what can cause problems?&lt;/p&gt;
&lt;p&gt;If you test an application, then in many cases you probably can&amp;rsquo;t test the &lt;code&gt;main&lt;/code&gt; function, especially when it starts a server for example.
This means, that the entry point to your application is untested, and this might cause problems in the future.
As a workaround, it is possible to mock the server starting functions so &lt;code&gt;main&lt;/code&gt; would return immediately.
This, probably, won&amp;rsquo;t return any meaningful values for the test itself, but you&amp;rsquo;ll be able to see what parts of code are triggered by the initialization process and see if there are some dead parts.
Alternatively, if all the main function does is initialization and service startup, these things can be tested separately, and &lt;code&gt;main&lt;/code&gt; can be excluded from testing, but this will lower the coverage percentage, as expected.&lt;/p&gt;
&lt;p&gt;Another problem is again solvable with mocks but seems to defeat the purpose of testing.
It does not.
I&amp;rsquo;m talking about interactions with external services.
A common misconception is that by mocking the server, we create an ideal model that doesn&amp;rsquo;t help at unit testing.
While this is somewhat true, it also depends on &lt;em&gt;how you mock it&lt;/em&gt;.
You can write such a mock that causes a timeout, or a mock that sends invalid or corrupted data to test error recovery.
You can even write a small service inside your codebase that mimics the public API of a real one, which can help with testing.
And with mock-based testing, your application code can mostly consist of private definitions, as you don&amp;rsquo;t need to export those for testing.&lt;/p&gt;
&lt;p&gt;This is something in between integration and unit testing, and while it&amp;rsquo;s surely not as good as real integration testing, it is still something worth doing.
Your public API probably won&amp;rsquo;t change as much as your code, and you&amp;rsquo;ll be more certain that nothing broke if you have such tests.
This is also a very good way of seeing what parts of your code in the coverage report are touched sorely by interacting with external services, without manually patching in the data for transformation.
Though I must say that writing mocks for every network problem imaginable is not an easy or even impossible task, it&amp;rsquo;s not always a solution.&lt;/p&gt;
&lt;h3 id=&#34;expression-and-line-coverage&#34;&gt;Expression and line coverage&lt;/h3&gt;
&lt;p&gt;The last main problem is that coverage tools are not always accurate.
This includes some quirks in the language runtime or tooling, that prevent it from tracking all lines or expressions.
I often see this in Lua, when I run some tests, collect coverage reports and see some branches as not being hit.
I then add a &lt;code&gt;print&lt;/code&gt; call to these branches and see prints in the output, meaning that the code is indeed touched by the test.
But the coverage tool doesn&amp;rsquo;t register it for some reason, and that particular call to &lt;code&gt;print&lt;/code&gt; is shown as uncovered.&lt;/p&gt;
&lt;p&gt;And the line-or-expression distinction in the coverage report is another problem with the opposite outcome.
If the previous problem was about the impossibility of achieving 100% because some code just won&amp;rsquo;t register, this is about achieving 100% without hitting all code.
The reason for that is that if you put multiple expressions on the same line, a coverage tool that doesn&amp;rsquo;t know about expressions may show a completely wrong coverage.
Here&amp;rsquo;s an oversimplified example:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;-- lib.lua&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold&#34;&gt;return&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  f = &lt;span style=&#34;font-weight:bold&#34;&gt;function&lt;/span&gt; (a, b)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;local&lt;/span&gt; res
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;font-weight:bold&#34;&gt;if&lt;/span&gt; a == 10 &lt;span style=&#34;font-weight:bold&#34;&gt;then&lt;/span&gt; res = a * b &lt;span style=&#34;font-weight:bold&#34;&gt;else&lt;/span&gt; res = a / b &lt;span style=&#34;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;font-weight:bold&#34;&gt;return&lt;/span&gt; res
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;-- lib_test.lua&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;local&lt;/span&gt; lib = require &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;lib&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;local&lt;/span&gt; res = lib.f(10, 20)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;assert(res == 200, &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;&amp;#34;expected 200, got &amp;#34;&lt;/span&gt; .. res)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;if&lt;/code&gt; statement in the function &lt;code&gt;f&lt;/code&gt; is written as a one-liner (for this particular example reasons).
The test only checks one specific case, which can only trigger the true branch of the &lt;code&gt;if&lt;/code&gt;.
There are no tests for false branch, which may easily result in a division by zero if &lt;code&gt;a&lt;/code&gt; is not &lt;code&gt;10&lt;/code&gt;, and &lt;code&gt;b&lt;/code&gt; is &lt;code&gt;0&lt;/code&gt;.
While this is not a problem in Lua, as it will just return &lt;code&gt;inf&lt;/code&gt;, this may be a problem for your application, which will not know what to do with infinity.&lt;/p&gt;
&lt;p&gt;This is an exaggerated example, only meant as an illustration of the problem.
Running the test module with &lt;a href=&#34;https://keplerproject.github.io/luacov/&#34; target=&#34;_blank&#34;&gt;Luacov&lt;/a&gt; shows a 100% coverage:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ lua -lluacov lib_test.lua &amp;amp;&amp;amp; luacov-console &amp;amp;&amp;amp; luacov-console -s
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;==============================================================================
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Summary
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;==============================================================================
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;File         Hits Missed Coverage
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;---------------------------------
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;lib.lua      4    0      100.00%
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;lib_test.lua 4    0      100.00%
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;---------------------------------
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Total        8    0      100.00%
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Thankfully, I write my code in Fennel, and it usually generates a somewhat well-formatted Lua code, so coverage is more or less accurate.
Doing the same with Clojure won&amp;rsquo;t have this problem at all, because &lt;a href=&#34;https://github.com/cloverage/cloverage&#34; target=&#34;_blank&#34;&gt;Cloverage&lt;/a&gt; provides two separate statistics - line coverage and form coverage:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-clojure&#34; data-lang=&#34;clojure&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;;; src/lib/core.clj&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;ns &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;lib.core&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;defn &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;f&lt;/span&gt; [&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;a&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;b&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  (&lt;span style=&#34;font-weight:bold&#34;&gt;if &lt;/span&gt;(&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;= &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;a&lt;/span&gt; 10) (&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;* &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;a&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;b&lt;/span&gt;) (&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;/ &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;a&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;b&lt;/span&gt;)))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;;; test/lib/core.clj&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;ns &lt;/span&gt;&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;lib.core-test&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  (&lt;span style=&#34;color:#666;font-style:italic&#34;&gt;:require&lt;/span&gt; [&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;clojure.test&lt;/span&gt; &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;:refer&lt;/span&gt; [&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;deftest&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;is&lt;/span&gt;]]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            [&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;lib.core&lt;/span&gt; &lt;span style=&#34;color:#666;font-style:italic&#34;&gt;:refer&lt;/span&gt; [&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;f&lt;/span&gt;]]))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;deftest&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;f-test&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  (&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;is&lt;/span&gt; (&lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;= &lt;/span&gt;200 (&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;f&lt;/span&gt; 10 20))))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As you can see, I&amp;rsquo;ve done the same thing as in Lua, but the coverage report suggests that something is not right:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ lein cloverage
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|-----------+---------+---------|
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;| Namespace | % Forms | % Lines |
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|-----------+---------+---------|
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|  lib.core |   75.00 |  100.00 |
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|-----------+---------+---------|
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;| ALL FILES |   75.00 |  100.00 |
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|-----------+---------+---------|
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Again, the main point I wanted to show here is that not all coverage percentage reports are fully trustable, that&amp;rsquo;s why the visual report is important.
Seeing a pretty long line with an &lt;code&gt;if&lt;/code&gt; statement in Lua should tell you to be more careful with this code, as some parts of the code could have not been touched.
Cloverage colors such line in orange, meaning that not all forms were covered, but doesn&amp;rsquo;t tell you which forms weren&amp;rsquo;t touched.
Both cases can be fixed with the proper formatting.&lt;/p&gt;
&lt;h2 id=&#34;closing-thoughts&#34;&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;Overall, I hope I was able to convince you that 60% code coverage is far from ideal and that 80% is still not enough.
In my personal opinion, something starting from 97% is a way to go.
Of course, it&amp;rsquo;s not always possible, but when it is, there&amp;rsquo;s no reason not to go for it.
And I&amp;rsquo;ll repeat myself, having higher coverage percentage doesn&amp;rsquo;t mean your code is well tested, it&amp;rsquo;s merely well covered.&lt;/p&gt;
&lt;p&gt;To summarize - test your public API and collect coverage of it specifically.
Having 100% code coverage achieved through such testing means that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;There&amp;rsquo;s no dead code.&lt;/li&gt;
&lt;li&gt;You have tests for everything in your application/library.&lt;/li&gt;
&lt;li&gt;You have a very reasonable starting point for improving all tests with more cases.
&lt;ul&gt;
&lt;li&gt;And because of that, there are more possibilities to find bugs in some far-to-reach places (80/20 rule).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&amp;rsquo;ve already said that I&amp;rsquo;m against writing unit tests for private functions, but, well, see for yourself.
I, personally, think that this means that the code was meant to be public, and making it private was a design mistake.
But your view may vary.&lt;/p&gt;
&lt;p&gt;While 100% coverage may not be always possible, carefully observe what kind of code was not covered.
If that&amp;rsquo;s something because the coverage calculation tool just misbehaves, well, there&amp;rsquo;s little you can do with it.
If that&amp;rsquo;s due to the inability to test something like a &lt;code&gt;main&lt;/code&gt; function, or a very tricky part that communicates with another service - make sure it&amp;rsquo;s well tested through integration testing.
Everything else, probably means you have some potentially incorrect design decisions that should be corrected.&lt;/p&gt;
&lt;p&gt;Hope this was an interesting read, feel free to share your thoughts on the topic and tag me in any of the &lt;a href=&#34;https://andreyor.st/about/#links&#34;&gt;social networks&lt;/a&gt; if you disagree or have something to discuss on the topic!&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;This is how you access a private var in Clojure.&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;&lt;div class=&#34;comment-link&#34;&gt; &lt;a href=&#34;mailto:%61%6e%64%72%65%79%6f%72%73%74%2b%62%6c%6f%67%40%67%6d%61%69%6c%2e%63%6f%6d?subject=Comment: 100% is the only acceptable test coverage&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Comment via email&lt;/a&gt;&lt;/div&gt;</description>
      <pubDate>Tue, 29 Mar 2022 18:09:00 +0300</pubDate>
    </item>
  </channel>
</rss>
