<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kaiven</title>
    <description>The latest articles on DEV Community by Kaiven (@arkanius_cc7c19c85664bebf).</description>
    <link>https://dev.to/arkanius_cc7c19c85664bebf</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4155984%2Fe7476611-d361-4307-abb3-506979c9fa26.jpg</url>
      <title>DEV Community: Kaiven</title>
      <link>https://dev.to/arkanius_cc7c19c85664bebf</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arkanius_cc7c19c85664bebf"/>
    <language>en</language>
    <item>
      <title>Your test fixtures are lying to you: what 13 real Minecraft crash reports taught me</title>
      <dc:creator>Kaiven</dc:creator>
      <pubDate>Thu, 01 Oct 2026 23:50:11 +0000</pubDate>
      <link>https://dev.to/arkanius_cc7c19c85664bebf/your-test-fixtures-are-lying-to-you-what-13-real-minecraft-crash-reports-taught-me-23pe</link>
      <guid>https://dev.to/arkanius_cc7c19c85664bebf/your-test-fixtures-are-lying-to-you-what-13-real-minecraft-crash-reports-taught-me-23pe</guid>
      <description>&lt;p&gt;I built a tool that reads modded Minecraft crash reports and tells you which mod killed the server. It passed every test I wrote and was still wrong about most real crashes.&lt;/p&gt;

&lt;p&gt;Here is what the real data taught me, in the order it hurt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your own samples will lie to you
&lt;/h2&gt;

&lt;p&gt;I wrote the tool first and the test fixtures second. Six synthetic logs, one per failure mode, each one crafted to contain exactly the string my pattern looked for. All six passed.&lt;/p&gt;

&lt;p&gt;Then I pointed it at an actual &lt;code&gt;crash-reports/&lt;/code&gt; folder from a live 234-mod server. It diagnosed &lt;strong&gt;three of eight&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is the test-writing equivalent of grading your own homework. I had written both the question and the answer, so of course they matched. The patterns were not wrong exactly — they were &lt;em&gt;narrow&lt;/em&gt;, tuned to a tidy example of each error rather than the shape those errors take in the wild. Five real crashes used wrapper exceptions, stale-jar &lt;code&gt;NoClassDefFoundError&lt;/code&gt;s and malformed resource IDs that my neat little samples never contained.&lt;/p&gt;

&lt;p&gt;The fix was not cleverness. It was reading thirteen real crash reports and writing rules for what was actually in them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The filename that contains two versions
&lt;/h2&gt;

&lt;p&gt;Forge ships as &lt;code&gt;forge-1.20.1-47.4.10-universal.jar&lt;/code&gt;. That is Minecraft 1.20.1, Forge 47.4.10.&lt;/p&gt;

&lt;p&gt;My regex to find the loader version was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?:neo)?forge[ \-](\d+\.\d+[\.\d]*)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reasonable looking. It reported every server's Forge version as &lt;strong&gt;1.20.1&lt;/strong&gt;, because that is the first number after &lt;code&gt;forge-&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Worse, it was &lt;em&gt;plausibly&lt;/em&gt; wrong. 1.20.1 is a real version string that appears everywhere in the log, so nothing looked broken. It took reading the output next to a crash report I already understood to notice the loader and the game had suspiciously identical versions.&lt;/p&gt;

&lt;p&gt;The fix is to prefer the authoritative line and only fall back to the filename:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pat&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Forge:\s*net\.(?:neo)?(?:minecraft)?forge:(?:forge:)?(\d+\.\d+[\.\d]*)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?:neo)?forge-\d+\.\d+(?:\.\d+)?-(\d+\.\d+[\.\d]*)-universal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?:neo)?forge[^\n]{0,24}?version[:\s]+(\d+\.\d+[\.\d]*)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;joined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Crash reports carry &lt;code&gt;Forge: net.minecraftforge:47.4.10&lt;/code&gt; in their system details. Use the thing that is unambiguous.&lt;/p&gt;

&lt;h2&gt;
  
  
  One line of HTML, 107,445 characters long
&lt;/h2&gt;

&lt;p&gt;The tool took &lt;strong&gt;59 seconds&lt;/strong&gt; on a real &lt;code&gt;debug.log&lt;/code&gt;. The file was 20,000 lines and 3.4 MB — that should be well under a second.&lt;/p&gt;

&lt;p&gt;My first theory was the obvious one. I measured the longest line in the file:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;107,445 characters. It was a complete GitHub HTML page. Some mod had fetched a URL and logged the entire response body on one line.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I capped line length for matching at 4,000 characters, re-ran, and the time went from 58.84s to &lt;strong&gt;58.84s&lt;/strong&gt;. Zero change. The theory was wrong, and the only reason I knew is that I measured before and after.&lt;/p&gt;

&lt;p&gt;So I stopped guessing and instrumented each stage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;read_log      0.04s
environment   0.01s
blame_mod     0.00s
diagnose    &amp;gt;115s     &amp;lt;- there it is
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then timed each rule individually. The first rule printed in 0.04s. The second never printed at all, because a single &lt;code&gt;re.search()&lt;/code&gt; call on one line had not returned.&lt;/p&gt;

&lt;p&gt;The pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?P&amp;lt;mod&amp;gt;\S+).*is client ?-?only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An unanchored &lt;code&gt;\S+&lt;/code&gt; followed by &lt;code&gt;.*&lt;/code&gt;. On a long line with no match, the engine tries every split of &lt;code&gt;\S+&lt;/code&gt; against every split of &lt;code&gt;.*&lt;/code&gt;. That is catastrophic backtracking, and it does not finish.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?P&amp;lt;mod&amp;gt;\S{1,80}) is client[ \-]?only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;58.84s to &lt;strong&gt;1.97s&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The lesson I actually took from this is not "avoid nested quantifiers". It is that I had a theory, it was wrong, and I only found out because I measured instead of declaring victory. The HTML line was real, interesting, and completely irrelevant.&lt;/p&gt;

&lt;p&gt;Now there is a test that fails loudly rather than hanging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_no_pattern_has_unbounded_quantifier_after_a_capture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\(\?P&amp;lt;\w+&amp;gt;\\S\+\)\.\*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;RULES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pat&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patterns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertIsNone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pat&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pat&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Blaming the right jar
&lt;/h2&gt;

&lt;p&gt;The genuinely useful trick, and the reason the tool is worth anything: Forge annotates every stack frame with the jar it came from.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;at net.mcreator.grimfauna.procedures.StalkerProcedure.execute
   (StalkerProcedure.java:250) ~[grimfauna-1.20.1-2.1.11.jar%23205!/:?]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So you can walk the trace, skip anything belonging to Minecraft, Forge or the JDK, and the first jar left is almost always the mod at fault.&lt;/p&gt;

&lt;p&gt;The whole feature lives in that skip list. A naive version blames &lt;code&gt;netty-common&lt;/code&gt; on half of all reports, because Netty sits near the top of any network-related trace. Then you fix that and it blames &lt;code&gt;fmlloader&lt;/code&gt;. Then &lt;code&gt;modlauncher&lt;/code&gt;. Every one of those was a real bug I shipped and caught by running it against reports whose cause I already knew.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same mistake, a second time, on a different tool
&lt;/h2&gt;

&lt;p&gt;I built a second tool that validates modpack IDs — the typo'd &lt;code&gt;minecraft:diamond_swrd&lt;/code&gt; that never crashes and simply gives the player nothing.&lt;/p&gt;

&lt;p&gt;Its first run on a real 233-mod pack reported &lt;strong&gt;169 typos&lt;/strong&gt;. Most were noise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apocalypsenow:alicepack
    did you mean:  apocalypsenow:icepick
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not a typo. That is fuzzy matching with no idea what it is doing.&lt;/p&gt;

&lt;p&gt;The cause was the same shape as the Forge version bug: I was comparing the &lt;strong&gt;whole ID&lt;/strong&gt;, so the shared &lt;code&gt;apocalypsenow:&lt;/code&gt; prefix — fourteen identical characters — inflated every score before the interesting part was even reached.&lt;/p&gt;

&lt;p&gt;Measured:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;compared&lt;/th&gt;
&lt;th&gt;full ID&lt;/th&gt;
&lt;th&gt;path only&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;cooked_caned_fish&lt;/code&gt; vs &lt;code&gt;cooked_canned_fish&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;0.984&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.971&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;alicepack&lt;/code&gt; vs &lt;code&gt;icepick&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;0.909&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.750&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Comparing paths alone separates a real typo from nonsense cleanly. 169 candidates became 63.&lt;/p&gt;

&lt;p&gt;And three of those are real, still live in a pack thousands of people play, confirmed against the mod's own jar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cooked_caned_fish&lt;/code&gt; → the item is &lt;code&gt;cooked_canned_fish&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cooked_canned_rabit_soup&lt;/code&gt; → the item is &lt;code&gt;cooked_canned_rabbit_soup&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stampler&lt;/code&gt; → the item is &lt;code&gt;stapler&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two of those sit in the pack's diet tags, so those foods silently have no diet category. Nothing crashes. Nothing logs. You find out when a player complains, if ever.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell myself at the start
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Write the fixtures from real data, or accept that your tests prove nothing.&lt;/strong&gt; A green suite built on invented inputs measures your imagination, not your code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Revert each fix and confirm the test fails.&lt;/strong&gt; I found one test that passed with its fix removed entirely. It had been quietly measuring nothing for days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you have a theory about a performance problem, measure before and after.&lt;/strong&gt; My plausible, interesting, well-researched theory moved the number by 0.00 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A tool that cries wolf gets ignored.&lt;/strong&gt; Findings are now tiered by confidence, and the low-confidence tier is hidden behind a flag. Sixty-three real candidates beats a hundred and sixty-nine you learn to scroll past.&lt;/p&gt;




&lt;p&gt;Both tools are single Python files with no dependencies. The free editions are on GitHub:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/Jaakoby/forge-server-doctor" rel="noopener noreferrer"&gt;forge-server-doctor&lt;/a&gt; — crash report diagnosis&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Jaakoby/pack-doctor" rel="noopener noreferrer"&gt;pack-doctor&lt;/a&gt; — modpack ID validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are paid versions with the full rule sets at &lt;a href="https://jaakoby.github.io/" rel="noopener noreferrer"&gt;jaakoby.github.io&lt;/a&gt;, but the free ones are standalone and the debugging stories above are the real point.&lt;/p&gt;

</description>
      <category>python</category>
      <category>testing</category>
      <category>debugging</category>
      <category>regex</category>
    </item>
  </channel>
</rss>
