https://mostlyobvio.us/Mostly Obvious.2023-12-02T15:00:00ZPaweł Pacanahttps://mostlyobvio.ustag:mostlyobvio.us,2023-12-02:/2023/11/temporary-databases-for-development/Temporary databases for development2023-12-02T15:00:00Z2023-12-02T15:00:00Z<h1 id="temporary_databases_for_development">Temporary databases for development</h1>
<p>At <a href="https://railseventstore.org">RailsEventStore</a> we have quite an extensive test suite to ensure that it runs smoothly on all supported database engines. That includes PostgreSQL, MySQL and Sqlite in several versions — not only newest but also the oldest-supported releases.</p>
<p>Setting up this many one-time databases and versions is now a mostly solved problem on CI, where each test run gets its own isolated environment. In development, at least on MacOS things are a bit more ambiguous.</p>
<p>Let's scope this problem a bit — you need to run a test suite for the database adapter on PostgreSQL 11 as well as PostgreSQL 15. There are several options.</p>
<ol>
<li><p>With <code>brew</code> that's a lot of gymnastics. First getting both versions installed at desired major versions. Then perhaps linking to switch currently chosen version, starting database service in the background, ensuring header files are in path to compile <code>pg</code> gem and so on. In the end you also have to babysit any accumulated database data.</p></li>
<li><p>An obvious solution seems to be introducing <code>docker</code>, right? Having many separate <code>Dockerfile</code> files describing database services in desired versions. Or just one starting many databases at different external ports from one <code>Dockerfile</code>. Any database state being discarded on container exit is a plus too. That already brings much needed convenience over plain <code>brew</code>. The only drawback is probably the performance — not great, not terrible.</p></li>
</ol>
<p>What if I told you there's a third option? And that database engines on UNIX-like systems already have that built-in?</p>
<h2 id="the_unix_way">The UNIX way</h2>
<p>Before revealing the solution let's briefly present the ingredients:</p>
<ol>
<li><p><em>Temporary files and directories</em> — with convenience of <code>mktemp</code> utility to generate unique and non-conflicting paths on disk. If these are created on <code>/tmp</code> partitions there's an additional benefit of operating system performing the cleanup periodically for us.</p></li>
<li><p><em>UNIX socket</em> — an inter-process data exchange mechanism, where the address is on the file system. With TCP sockets one would address it by <code>host:port</code>, where the communication goes through IP stack and routing. Instead here we "connect" to the path on disk. The access is controlled by disk permissions too. An example of such address is <code>/tmp/tmp.iML7fAcubU</code>.</p></li>
<li><p><em>Operating system process</em> — our smallest unit of isolation. Such processes are identified by PID numbers. Knowing such identifier lets us control the process after we send it into the background.</p></li>
</ol>
<p>Knowing all this, here's the raw solution:</p>
<div class="highlight"><pre class="highlight shell"><code><span class="nv">TMP</span><span class="o">=</span><span class="si">$(</span><span class="nb">mktemp</span> <span class="nt">-d</span><span class="si">)</span>
<span class="nv">DB</span><span class="o">=</span><span class="nv">$TMP</span>/db
<span class="nv">SOCKET</span><span class="o">=</span><span class="nv">$TMP</span>
initdb <span class="nt">-D</span> <span class="nv">$DB</span>
pg_ctl <span class="nt">-D</span> <span class="nv">$DB</span> <span class="se">\</span>
<span class="nt">-l</span> <span class="nv">$TMP</span>/logfile <span class="se">\</span>
<span class="nt">-o</span> <span class="s2">"--unix_socket_directories='</span><span class="nv">$SOCKET</span><span class="s2">'"</span> <span class="se">\</span>
<span class="nt">-o</span> <span class="s2">"--listen_addresses=''</span><span class="se">\'</span><span class="s2">''</span><span class="se">\'</span><span class="s2">"</span> <span class="se">\</span>
start
createdb <span class="nt">-h</span> <span class="nv">$SOCKET</span> rails_event_store
<span class="nb">export </span><span class="nv">DATABASE_URL</span><span class="o">=</span><span class="s2">"postgresql:///rails_event_store?host=</span><span class="nv">$SOCKET</span><span class="s2">"</span>
</code></pre></div>
<p>First we create a temporary base directory with <code>mktemp -d</code>. What we get from it is some random and unique path, i.e. <code>/tmp/tmp.iML7fAcubU</code>. This is the base directory under which we'll host UNIX socket, database storage files and logs that database process produces when running in the background.</p>
<p>Next the database storage has to be seeded with <code>initdb</code> at the designated directory. Then a postgres process is started via <code>pg_ctl</code> in the background. It is just enough to configure with command line switches. These tell, in order — where the logs should live, that we communicate with other process via UNIX socket at given path and that no TCP socket is needed. Thus there will be no conflict of different processes competing for the same <code>host:port</code> pair.</p>
<p>Once our isolated database engine unit is running, it would be useful to prepare application environment. Creating the database with <code>createdb</code> PostgreSQL CLI which understands UNIX sockets too. Finally letting the application know where its database is by exporting <code>DATABSE_URL</code> environment variable. The URL completely describing a particular instance of database engine in chosen version may look like this — <code>postgresql:///rails_event_store?host=/tmp/tmp.iML7fAcubU</code>.</p>
<p>Once we're done with testing it is time to nuke our temporary database. Killing the process in the background first. Then removing temporary directory root it operated in.</p>
<div class="highlight"><pre class="highlight shell"><code>pg_ctl <span class="nt">-D</span> <span class="nv">$DB</span> stop
<span class="nb">rm</span> <span class="nt">-rf</span> <span class="nv">$TMP</span>
</code></pre></div>
<p>And that's mostly it.</p>
<h2 id="little_automation_goes_a_long_way">Little automation goes a long way</h2>
<p>It would be such a nice thing to have a shell function that spawns a temporary database engine in the background, leaving us in the shell with <code>DATABASE_URL</code> already set and cleaning up automatically when we exit.</p>
<p>The only missing ingredient is an exit hook for the shell. One can be implemented with <code>trap</code> and stack-like behaviour built on top of it, as in <a href="https://github.com/modernish/modernish#user-content-use-varstacktrap">modernish</a>:</p>
<div class="highlight"><pre class="highlight shell"><code>pushtrap <span class="o">()</span> <span class="o">{</span>
<span class="nb">test</span> <span class="s2">"</span><span class="nv">$traps</span><span class="s2">"</span> <span class="o">||</span> <span class="nb">trap</span> <span class="s1">'set +eu; eval $traps'</span> 0<span class="p">;</span>
<span class="nv">traps</span><span class="o">=</span><span class="s2">"</span><span class="nv">$*</span><span class="s2">; </span><span class="nv">$traps</span><span class="s2">"</span>
<span class="o">}</span>
</code></pre></div>
<p>The automation in its full shape:</p>
<div class="highlight"><pre class="highlight shell"><code>with_postgres_15<span class="o">()</span> <span class="o">{</span>
<span class="o">(</span>
pushtrap<span class="o">()</span> <span class="o">{</span>
<span class="nb">test</span> <span class="s2">"</span><span class="nv">$traps</span><span class="s2">"</span> <span class="o">||</span> <span class="nb">trap</span> <span class="s1">'set +eu; eval $traps'</span> 0<span class="p">;</span>
<span class="nv">traps</span><span class="o">=</span><span class="s2">"</span><span class="nv">$*</span><span class="s2">; </span><span class="nv">$traps</span><span class="s2">"</span>
<span class="o">}</span>
<span class="nv">TMP</span><span class="o">=</span><span class="si">$(</span><span class="nb">mktemp</span> <span class="nt">-d</span><span class="si">)</span>
<span class="nv">DB</span><span class="o">=</span><span class="nv">$TMP</span>/db
<span class="nv">SOCKET</span><span class="o">=</span><span class="nv">$TMP</span>
/path_to_pg_15/initdb <span class="nt">-D</span> <span class="nv">$DB</span>
/path_to_pg_15/pg_ctl <span class="nt">-D</span> <span class="nv">$DB</span> <span class="se">\</span>
<span class="nt">-l</span> <span class="nv">$TMP</span>/logfile <span class="se">\</span>
<span class="nt">-o</span> <span class="s2">"--unix_socket_directories='</span><span class="nv">$SOCKET</span><span class="s2">'"</span> <span class="se">\</span>
<span class="nt">-o</span> <span class="s2">"--listen_addresses=''</span><span class="se">\'</span><span class="s2">''</span><span class="se">\'</span><span class="s2">"</span> <span class="se">\</span>
start
/path_to_pg_15/createdb <span class="nt">-h</span> <span class="nv">$SOCKET</span> rails_event_store
<span class="nb">export </span><span class="nv">DATABASE_URL</span><span class="o">=</span><span class="s2">"postgresql:///rails_event_store?host=</span><span class="nv">$SOCKET</span><span class="s2">"</span>
pushtrap <span class="s2">"/path_to_pg_15/pg_ctl -D </span><span class="nv">$DB</span><span class="s2"> stop; rm -rf </span><span class="nv">$TMP</span><span class="s2">"</span> EXIT
<span class="nv">$SHELL</span>
<span class="o">)</span>
<span class="o">}</span>
</code></pre></div>
<p>Whenever I need to be dropped into a shell with Postgres 15 running, executing <code>with_postgres_15</code> fulfills it.</p>
<h2 id="the_nix_dessert">The nix dessert</h2>
<p>One may argue that using <code>Docker</code> is familiar and temporary databases is a solved problem there. I agree with that sentiment at large.</p>
<p>However I've found my peace with <code>nix</code> long time ago. Thanks to <a href="https://opencollective.com/nix-macos">numerous contributions and initiatives</a> using <code>nix</code> on MacOS is nowadays as simple as <code>brew</code>.</p>
<p>With <a href="https://nix.dev">nix manager</a> and <code>nix-shell</code> utility, I'm currently spawning the databases with one command. That is:</p>
<div class="highlight"><pre class="highlight shell"><code>nix-shell ~/Code/rails_event_store/support/nix/postgres_15.nix
</code></pre></div>
<p>As an added bonus to previous script, this will fetch PostgreSQL binaries from nix repository when they're not already on my system in given version. All the convenience of Docker without any of its drawbacks in a tailor-made use case.</p>
<div class="highlight"><pre class="highlight nix"><code><span class="kn">with</span> <span class="kr">import</span> <span class="o"><</span><span class="nv">nixpkgs</span><span class="o">></span> <span class="p">{};</span>
<span class="nv">mkShell</span> <span class="p">{</span>
<span class="nv">buildInputs</span> <span class="o">=</span> <span class="p">[</span> <span class="nv">postgresql_14</span> <span class="p">];</span>
<span class="nv">shellHook</span> <span class="o">=</span> <span class="s2">''</span><span class="err">
</span><span class="s2"> </span><span class="si">${</span><span class="kr">builtins</span><span class="o">.</span><span class="nv">readFile</span> <span class="sx">./pushtrap.sh</span><span class="si">}</span><span class="err">
</span><span class="s2"> TMP=$(mktemp -d)</span><span class="err">
</span><span class="s2"> DB=$TMP/db</span><span class="err">
</span><span class="s2"> SOCKET=$TMP</span><span class="err">
</span><span class="s2"> initdb -D $DB</span><span class="err">
</span><span class="s2"> pg_ctl -D $DB \</span><span class="err">
</span><span class="s2"> -l $TMP/logfile \</span><span class="err">
</span><span class="s2"> -o "--unix_socket_directories='$SOCKET'" \</span><span class="err">
</span><span class="s2"> -o "--listen_addresses=</span><span class="se">''\'''\'</span><span class="s2">" \</span><span class="err">
</span><span class="s2"> start</span><span class="err">
</span><span class="s2"> createdb -h $SOCKET rails_event_store</span><span class="err">
</span><span class="s2"> export DATABASE_URL="postgresql:///rails_event_store?host=$SOCKET"</span><span class="err">
</span><span class="s2"> pushtrap "pg_ctl -D $DB stop; rm -rf $TMP" EXIT</span><span class="err">
</span><span class="s2"> ''</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p>In RailsEventStore we've prepared such <a href="https://github.com/RailsEventStore/rails_event_store/tree/master/support/nix">expressions for numerous PostgreSQL, MySQL and Redis versions</a>. They're already useful in development and we'll eventually take advantage of them on our CI.</p>
<p>Happy experimenting!</p>
tag:mostlyobvio.us,2023-09-13:/2023/09/six-ways-to-prevent-a-monkey-patch-drift-from-the-original-code/Six ways to prevent a monkey-patch drift from the original code2023-09-13T08:00:00Z2023-09-13T08:00:00Z<h1 id="six_ways_to_prevent_a_monkey_patch_drift_from_the_original_code">Six ways to prevent a monkey-patch drift from the original code</h1>
<p>Monkey-patching in short is modifying external code, whose source we don't directly control, to fit our specific purpose in the project. When modernising framework stack in "legacy" projects this is often a necessity when an upgrade of a dependency is not yet possible or would involve moving too many blocks at once.</p>
<p>It's a short-term solution to move things forward. The reward we get from monkey-patching is instant. The code is changed without asking for anyone's permission and without much extra work that a dependency fork would involve.</p>
<p>But it comes with a hefty price of being very brittle. We absolutely cannot expect that a monkey-patch would work with any future versions of the external dependency. Thus communicating this short-term loan is crucial when we're not soloing.</p>
<h2 id="guarding_patched_dependency_changes_with_version_check">Guarding patched dependency changes with version check</h2>
<p>One way to communicate a monkey-patched dependency is to document it with a test.</p>
<p>Why test?</p>
<ol>
<li><p>It is close to the changed code — in the project source, as opposed to any external documentation medium.</p></li>
<li><p>It is executable, unlike code comment and greatly reduces the risk of someone <a href="https://www.goodreads.com/quotes/379100-there-s-no-point-in-acting-surprised-about-it-all-the">not noticing an announcement</a>.</p></li>
</ol>
<p>In a project I've recently worked on there was already an unannounced <code>AcitveRecord::Persistence#reload</code> method patch inside <code>User</code> model. I consider myself very lucky spotting within over 910000 lines of code having only 10% test coverage.</p>
<p>A code comment would definitely not help me notice it — I came to this project only recently and the authors were already working on it before were gone.</p>
<p>A test I've added to document it looked like this:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># spec/models/user_spec.rb</span>
<span class="nb">require</span> <span class="s2">"rails_helper"</span>
<span class="no">RSpec</span><span class="p">.</span><span class="nf">describe</span> <span class="s2">"User"</span> <span class="k">do</span>
<span class="n">specify</span> <span class="s2">"#reload method is overridden based on framework implementation"</span> <span class="k">do</span>
<span class="n">expect</span><span class="p">(</span><span class="no">Rails</span><span class="p">.</span><span class="nf">version</span><span class="p">).</span><span class="nf">to</span> <span class="n">eq</span><span class="p">(</span><span class="s2">"5.1.7"</span><span class="p">),</span> <span class="n">failure_message</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">failure_message</span>
<span class="o"><<~</span><span class="no">WARN</span><span class="sh">
It looks like you upgraded Rails.
Check if User#reload method body corresponds to the current Rails version implementation of rails/activerecord/lib/active_record/persistence.rb#reload.
When it's ready bump the version in this condition.
</span><span class="no"> WARN</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p>Now whenever Rails version changes, this check is supposed to fail. The failure has a descriptive message instructing what needs to be checked in order to prolong the patch.</p>
<p>Within an organisation relying on continuous integration and aspiring to the testing culture this should be enough to prevent failure from such patching.</p>
<p>But is it enough developer-friendly?</p>
<h2 id="improving_version_check">Improving version check</h2>
<p>One drawback of strict version checks is ...being too strict. Some dependencies are best allowed within a range of possible versions. To not fail the check on version changes mitigating security issues for example:</p>
<div class="highlight"><pre class="highlight plaintext"><code>--- spec/app/models/user_spec.rb
+++ spec/app/models/user_spec.rb
@@ -4,7 +4,7 @@ require 'rails_helper'
- expect(Rails.version).to eq('7.0.7'), failure_message
+ expect(Rails.version).to eq('7.0.7.2'), failure_message
+ # Mitigate CVE-2023-38037
+ # https://discuss.rubyonrails.org/t/cve-2023-38037-possible-file-disclosure-of-locally-encrypted-files/83544
</code></pre></div>
<p>When we're certain that a dependency follows a meaningful version numbering scheme, we can change the check to verify more relaxed version constraints.</p>
<p>An example using RubyGems API:</p>
<div class="highlight"><pre class="highlight plaintext"><code>--- spec/app/models/user_spec.rb
+++ spec/app/models/user_spec.rb
@@ -4,7 +4,7 @@ require 'rails_helper'
- expect(Rails.version).to eq('7.0.7.2'), failure_message
+ expect(Gem::Requirement.create('~> 7.0.0').satisfied_by?(Gem::Version.create(Rails.version))).to eq(true), failure_message
</code></pre></div>
<p>But do we know for sure that a security-patch-release does not change the code we've patched?</p>
<h2 id="checking_if_the_source_did_not_change">Checking if the source did not change</h2>
<p>It would be ideal if we could peek into the source of the method we're patching and tell if it has changed since the original one.</p>
<p>Reading a tweet from my <a href="https://blog.arkency.com/authors/robert-pankowecki/">former arkency fellow</a> shed new light on the issue.</p>
<p><a href="https://twitter.com/pankowecki/status/1687103206713962497" target="_blank" rel="noreferrer">
<img src="<%= src_fit("six-ways-mp/tweet-1687103206713962497.png") %>" width="100%" loading="lazy">
</a></p>
<p>A complete test utilising this technique may look like this:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># spec/models/user_spec.rb</span>
<span class="nb">require</span> <span class="s2">"rails_helper"</span>
<span class="no">RSpec</span><span class="p">.</span><span class="nf">describe</span> <span class="s2">"User"</span> <span class="k">do</span>
<span class="n">specify</span> <span class="s2">"#reload method is overridden based on framework implementation"</span> <span class="k">do</span>
<span class="n">expect</span><span class="p">(</span><span class="n">checksum_of_actual_reload_implementation</span><span class="p">).</span><span class="nf">to</span> <span class="n">eq</span><span class="p">(</span>
<span class="n">checksum_of_expected_reload_implementation</span><span class="p">,</span>
<span class="p">),</span>
<span class="n">failure_message</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">checksum_of_actual_reload_implementation</span>
<span class="no">Digest</span><span class="o">::</span><span class="no">SHA256</span><span class="p">.</span><span class="nf">hexdigest</span><span class="p">(</span>
<span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Persistence</span><span class="p">.</span><span class="nf">instance_method</span><span class="p">(</span><span class="ss">:reload</span><span class="p">).</span><span class="nf">source</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">checksum_of_expected_reload_implementation</span>
<span class="s2">"3bf4f24fb7f24f75492b979f7643c78d6ddf8b9f5fbd182f82f3dd3d4c9f1600"</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">failure_message</span>
<span class="c1">#...</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p>The little disappointment came shortly after I've realised <a href="https://twitter.com/mostlyobvious/status/1694700843629478389">Method#source is not a standard Ruby</a>. It is from a <code>method_source</code> <a href="https://github.com/banister/method_source/tree/master">dependency</a> that came to the project I've worked on indirectly via <code>pry</code>. Nevertheless it worked within the scope of existing project dependencies and was better than a plain version check.</p>
<p>Can we do any better?</p>
<h2 id="checking_abstract_syntax_tree_of_the_implementation">Checking Abstract Syntax Tree of the implementation</h2>
<p>I admit that computing the hash of the source code is neat. However, it falls short of "formatting" changes. Source code is a textual representation. Introducing whitespace characters — spaces or line breaks does not change the implementation. It behaves the same. The hash will be different though, raising a false negative.</p>
<p>So can we do it better? Yes, we can. With little help of <a href="https://en.wikipedia.org/wiki/Abstract_syntax_tree">AST</a>. In theory AST representation should free us from how the patched code is formatted.</p>
<p>In Ruby, we have a few options to render AST of the source code. The popular <code>parser</code> and <code>syntax_tree</code> gems. The <code>Ripper</code> in the standard library. Or the native <code>RubyVM::AbstractSyntaxTree</code>.</p>
<p>A pessimist may notice their limitations first:</p>
<ul>
<li><p><code>RubyVM::AbstractSyntaxTree</code> and <code>Ripper</code> still include formatting in the output, defeating the purpose</p></li>
<li><p><code>parser</code> and <code>syntax_tree</code> are <a href="https://github.com/whitequark/parser">external</a> <a href="https://github.com/ruby-syntax-tree/syntax_tree">dependencies</a>, so not universally applicable — chances are they're already a transitive dependency in your project</p></li>
</ul>
<p>I definitely did not see it all at first sight. Here are the implementations I would not recommend.</p>
<h3 id="false_hopes_for_checksum_free_from_formatting">False hopes for checksum free from formatting</h3>
<p>In core Ruby there is <code>RubyVM::AbstractSyntaxTree</code> <a href="https://ruby-doc.org/core-trunk/RubyVM/AbstractSyntaxTree.html">module</a>, which provides methods to parse Ruby code into abstract syntax trees. Unfortunately, the output includes line and column information, making it unfit for checksumming independent of source formatting. Thus it is not better in any aspect than hexdigest on plain source code.</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># spec/models/user_spec.rb</span>
<span class="nb">require</span> <span class="s2">"rails_helper"</span>
<span class="no">RSpec</span><span class="p">.</span><span class="nf">describe</span> <span class="s2">"User"</span> <span class="k">do</span>
<span class="n">specify</span> <span class="s2">"#reload method is overridden based on framework implementation"</span> <span class="k">do</span>
<span class="n">expect</span><span class="p">(</span><span class="n">checksum_of_actual_reload_implementation</span><span class="p">).</span><span class="nf">to</span> <span class="n">eq</span><span class="p">(</span>
<span class="n">checksum_of_expected_reload_implementation</span><span class="p">,</span>
<span class="p">),</span>
<span class="n">failure_message</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">checksum_of_actual_reload_implementation</span>
<span class="no">Digest</span><span class="o">::</span><span class="no">SHA256</span><span class="p">.</span><span class="nf">hexdigest</span><span class="p">(</span>
<span class="no">RubyVM</span><span class="o">::</span><span class="no">AbstractSyntaxTree</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span>
<span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Persistence</span><span class="p">.</span><span class="nf">instance_method</span><span class="p">(</span><span class="ss">:reload</span><span class="p">).</span><span class="nf">source</span><span class="p">,</span>
<span class="p">).</span><span class="nf">pretty_print_inspect</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">checksum_of_expected_reload_implementation</span>
<span class="s2">"ed2f4fdf62aece74173a44a65d8919ecf3e0fca7a5d38e2cefb9e51c408a4ab4"</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div><h3 id="no_checksum_for_the_added_benefit_of_seeing_actual_implementation_changes">No checksum for the added benefit of seeing actual implementation changes</h3>
<p>In Ruby standard library we may also find <code>Ripper</code>, a <a href="https://ruby-doc.org/stdlib-3.0.0/libdoc/ripper/rdoc/Ripper.html">Ruby script parser</a>. It parses the code into a <a href="https://en.wikipedia.org/wiki/S-expression">symbolic expression tree</a>. Unfortunately this too contains line and column information in the output. Perhaps with some additional post-processing step, we could get rid of it. I prefer comparing s-expressions to checksums — the test framework has a chance to show differences in the compared syntax trees. Which is a nice bonus!</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># spec/models/user_spec.rb</span>
<span class="nb">require</span> <span class="s2">"rails_helper"</span>
<span class="no">RSpec</span><span class="p">.</span><span class="nf">describe</span> <span class="s2">"User"</span> <span class="k">do</span>
<span class="n">specify</span> <span class="s2">"#reload method is overridden based on framework implementation"</span> <span class="k">do</span>
<span class="n">expect</span><span class="p">(</span><span class="n">actual_find_record_implementation</span><span class="p">).</span><span class="nf">to</span> <span class="n">eq</span><span class="p">(</span>
<span class="n">expected_find_record_implementation</span><span class="p">,</span>
<span class="p">),</span>
<span class="n">failure_message</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">actual_reload_implementation</span>
<span class="no">Ripper</span><span class="p">.</span><span class="nf">sexp</span><span class="p">(</span><span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Persistence</span><span class="p">.</span><span class="nf">instance_method</span><span class="p">(</span><span class="ss">:reload</span><span class="p">).</span><span class="nf">source</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">expected_reload_implementation</span>
<span class="p">[</span>
<span class="ss">:program</span><span class="p">,</span>
<span class="p">[</span>
<span class="p">[</span>
<span class="ss">:def</span><span class="p">,</span>
<span class="p">[</span><span class="ss">:@ident</span><span class="p">,</span> <span class="s2">"reload"</span><span class="p">,</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">8</span><span class="p">]],</span>
<span class="p">[</span>
<span class="ss">:paren</span><span class="p">,</span>
<span class="p">[</span>
<span class="ss">:params</span><span class="p">,</span>
<span class="kp">nil</span><span class="p">,</span>
<span class="p">[</span>
<span class="p">[</span>
<span class="p">[</span><span class="ss">:@ident</span><span class="p">,</span> <span class="s2">"options"</span><span class="p">,</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">15</span><span class="p">]],</span>
<span class="p">[</span><span class="ss">:var_ref</span><span class="p">,</span> <span class="p">[</span><span class="ss">:@kw</span><span class="p">,</span> <span class="s2">"nil"</span><span class="p">,</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">25</span><span class="p">]]],</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="kp">nil</span><span class="p">,</span>
<span class="kp">nil</span><span class="p">,</span>
<span class="kp">nil</span><span class="p">,</span>
<span class="kp">nil</span><span class="p">,</span>
<span class="kp">nil</span><span class="p">,</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="p">[</span>
<span class="ss">:bodystmt</span><span class="p">,</span>
<span class="p">[</span>
<span class="p">[</span>
<span class="ss">:call</span><span class="p">,</span>
<span class="p">[</span>
<span class="ss">:call</span><span class="p">,</span>
<span class="p">[</span>
<span class="ss">:call</span><span class="p">,</span>
<span class="p">[</span><span class="ss">:var_ref</span><span class="p">,</span> <span class="p">[</span><span class="ss">:@kw</span><span class="p">,</span> <span class="s2">"self"</span><span class="p">,</span> <span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">6</span><span class="p">]]],</span>
<span class="p">[</span><span class="ss">:@period</span><span class="p">,</span> <span class="s2">"."</span><span class="p">,</span> <span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">10</span><span class="p">]],</span>
<span class="p">[</span><span class="ss">:@ident</span><span class="p">,</span> <span class="s2">"class"</span><span class="p">,</span> <span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">11</span><span class="p">]],</span>
<span class="p">],</span>
<span class="p">[</span><span class="ss">:@period</span><span class="p">,</span> <span class="s2">"."</span><span class="p">,</span> <span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">16</span><span class="p">]],</span>
<span class="p">[</span><span class="ss">:@ident</span><span class="p">,</span> <span class="s2">"connection"</span><span class="p">,</span> <span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">17</span><span class="p">]],</span>
<span class="p">],</span>
<span class="p">[</span><span class="ss">:@period</span><span class="p">,</span> <span class="s2">"."</span><span class="p">,</span> <span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">27</span><span class="p">]],</span>
<span class="p">[</span><span class="ss">:@ident</span><span class="p">,</span> <span class="s2">"clear_query_cache"</span><span class="p">,</span> <span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">28</span><span class="p">]],</span>
<span class="p">],</span>
<span class="p">[</span>
<span class="ss">:assign</span><span class="p">,</span>
<span class="p">[</span><span class="ss">:var_field</span><span class="p">,</span> <span class="p">[</span><span class="ss">:@ident</span><span class="p">,</span> <span class="s2">"fresh_object"</span><span class="p">,</span> <span class="p">[</span><span class="mi">4</span><span class="p">,</span> <span class="mi">6</span><span class="p">]]],</span>
<span class="p">[</span>
<span class="ss">:if</span><span class="p">,</span>
<span class="p">[</span>
<span class="ss">:method_add_arg</span><span class="p">,</span>
<span class="p">[</span><span class="ss">:fcall</span><span class="p">,</span> <span class="p">[</span><span class="ss">:@ident</span><span class="p">,</span> <span class="s2">"apply_scoping?"</span><span class="p">,</span> <span class="p">[</span><span class="mi">4</span><span class="p">,</span> <span class="mi">24</span><span class="p">]]],</span>
<span class="p">[</span>
<span class="ss">:arg_paren</span><span class="p">,</span>
<span class="p">[</span>
<span class="ss">:args_add_block</span><span class="p">,</span>
<span class="p">[[</span><span class="ss">:var_ref</span><span class="p">,</span> <span class="p">[</span><span class="ss">:@ident</span><span class="p">,</span> <span class="s2">"options"</span><span class="p">,</span> <span class="p">[</span><span class="mi">4</span><span class="p">,</span> <span class="mi">39</span><span class="p">]]]],</span>
<span class="kp">false</span><span class="p">,</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="p">[</span>
<span class="p">[</span>
<span class="ss">:method_add_arg</span><span class="p">,</span>
<span class="p">[</span><span class="ss">:fcall</span><span class="p">,</span> <span class="p">[</span><span class="ss">:@ident</span><span class="p">,</span> <span class="s2">"_find_record"</span><span class="p">,</span> <span class="p">[</span><span class="mi">5</span><span class="p">,</span> <span class="mi">8</span><span class="p">]]],</span>
<span class="p">[</span>
<span class="ss">:arg_paren</span><span class="p">,</span>
<span class="p">[</span>
<span class="ss">:args_add_block</span><span class="p">,</span>
<span class="p">[[</span><span class="ss">:var_ref</span><span class="p">,</span> <span class="p">[</span><span class="ss">:@ident</span><span class="p">,</span> <span class="s2">"options"</span><span class="p">,</span> <span class="p">[</span><span class="mi">5</span><span class="p">,</span> <span class="mi">21</span><span class="p">]]]],</span>
<span class="kp">false</span><span class="p">,</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="p">[</span>
<span class="ss">:else</span><span class="p">,</span>
<span class="p">[</span>
<span class="p">[</span>
<span class="ss">:method_add_block</span><span class="p">,</span>
<span class="p">[</span>
<span class="ss">:call</span><span class="p">,</span>
<span class="p">[</span>
<span class="ss">:call</span><span class="p">,</span>
<span class="p">[</span><span class="ss">:var_ref</span><span class="p">,</span> <span class="p">[</span><span class="ss">:@kw</span><span class="p">,</span> <span class="s2">"self"</span><span class="p">,</span> <span class="p">[</span><span class="mi">7</span><span class="p">,</span> <span class="mi">8</span><span class="p">]]],</span>
<span class="p">[</span><span class="ss">:@period</span><span class="p">,</span> <span class="s2">"."</span><span class="p">,</span> <span class="p">[</span><span class="mi">7</span><span class="p">,</span> <span class="mi">12</span><span class="p">]],</span>
<span class="p">[</span><span class="ss">:@ident</span><span class="p">,</span> <span class="s2">"class"</span><span class="p">,</span> <span class="p">[</span><span class="mi">7</span><span class="p">,</span> <span class="mi">13</span><span class="p">]],</span>
<span class="p">],</span>
<span class="p">[</span><span class="ss">:@period</span><span class="p">,</span> <span class="s2">"."</span><span class="p">,</span> <span class="p">[</span><span class="mi">7</span><span class="p">,</span> <span class="mi">18</span><span class="p">]],</span>
<span class="p">[</span><span class="ss">:@ident</span><span class="p">,</span> <span class="s2">"unscoped"</span><span class="p">,</span> <span class="p">[</span><span class="mi">7</span><span class="p">,</span> <span class="mi">19</span><span class="p">]],</span>
<span class="p">],</span>
<span class="p">[</span>
<span class="ss">:brace_block</span><span class="p">,</span>
<span class="kp">nil</span><span class="p">,</span>
<span class="p">[</span>
<span class="p">[</span>
<span class="ss">:method_add_arg</span><span class="p">,</span>
<span class="p">[</span><span class="ss">:fcall</span><span class="p">,</span> <span class="p">[</span><span class="ss">:@ident</span><span class="p">,</span> <span class="s2">"_find_record"</span><span class="p">,</span> <span class="p">[</span><span class="mi">7</span><span class="p">,</span> <span class="mi">30</span><span class="p">]]],</span>
<span class="p">[</span>
<span class="ss">:arg_paren</span><span class="p">,</span>
<span class="p">[</span>
<span class="ss">:args_add_block</span><span class="p">,</span>
<span class="p">[[</span><span class="ss">:var_ref</span><span class="p">,</span> <span class="p">[</span><span class="ss">:@ident</span><span class="p">,</span> <span class="s2">"options"</span><span class="p">,</span> <span class="p">[</span><span class="mi">7</span><span class="p">,</span> <span class="mi">43</span><span class="p">]]]],</span>
<span class="kp">false</span><span class="p">,</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="p">[</span>
<span class="ss">:assign</span><span class="p">,</span>
<span class="p">[</span><span class="ss">:var_field</span><span class="p">,</span> <span class="p">[</span><span class="ss">:@ivar</span><span class="p">,</span> <span class="s2">"@association_cache"</span><span class="p">,</span> <span class="p">[</span><span class="mi">10</span><span class="p">,</span> <span class="mi">6</span><span class="p">]]],</span>
<span class="p">[</span>
<span class="ss">:method_add_arg</span><span class="p">,</span>
<span class="p">[</span>
<span class="ss">:call</span><span class="p">,</span>
<span class="p">[</span><span class="ss">:var_ref</span><span class="p">,</span> <span class="p">[</span><span class="ss">:@ident</span><span class="p">,</span> <span class="s2">"fresh_object"</span><span class="p">,</span> <span class="p">[</span><span class="mi">10</span><span class="p">,</span> <span class="mi">27</span><span class="p">]]],</span>
<span class="p">[</span><span class="ss">:@period</span><span class="p">,</span> <span class="s2">"."</span><span class="p">,</span> <span class="p">[</span><span class="mi">10</span><span class="p">,</span> <span class="mi">39</span><span class="p">]],</span>
<span class="p">[</span><span class="ss">:@ident</span><span class="p">,</span> <span class="s2">"instance_variable_get"</span><span class="p">,</span> <span class="p">[</span><span class="mi">10</span><span class="p">,</span> <span class="mi">40</span><span class="p">]],</span>
<span class="p">],</span>
<span class="p">[</span>
<span class="ss">:arg_paren</span><span class="p">,</span>
<span class="p">[</span>
<span class="ss">:args_add_block</span><span class="p">,</span>
<span class="p">[</span>
<span class="p">[</span>
<span class="ss">:symbol_literal</span><span class="p">,</span>
<span class="p">[</span><span class="ss">:symbol</span><span class="p">,</span> <span class="p">[</span><span class="ss">:@ivar</span><span class="p">,</span> <span class="s2">"@association_cache"</span><span class="p">,</span> <span class="p">[</span><span class="mi">10</span><span class="p">,</span> <span class="mi">63</span><span class="p">]]],</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="kp">false</span><span class="p">,</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="p">[</span>
<span class="ss">:assign</span><span class="p">,</span>
<span class="p">[</span><span class="ss">:var_field</span><span class="p">,</span> <span class="p">[</span><span class="ss">:@ivar</span><span class="p">,</span> <span class="s2">"@attributes"</span><span class="p">,</span> <span class="p">[</span><span class="mi">11</span><span class="p">,</span> <span class="mi">6</span><span class="p">]]],</span>
<span class="p">[</span>
<span class="ss">:method_add_arg</span><span class="p">,</span>
<span class="p">[</span>
<span class="ss">:call</span><span class="p">,</span>
<span class="p">[</span><span class="ss">:var_ref</span><span class="p">,</span> <span class="p">[</span><span class="ss">:@ident</span><span class="p">,</span> <span class="s2">"fresh_object"</span><span class="p">,</span> <span class="p">[</span><span class="mi">11</span><span class="p">,</span> <span class="mi">20</span><span class="p">]]],</span>
<span class="p">[</span><span class="ss">:@period</span><span class="p">,</span> <span class="s2">"."</span><span class="p">,</span> <span class="p">[</span><span class="mi">11</span><span class="p">,</span> <span class="mi">32</span><span class="p">]],</span>
<span class="p">[</span><span class="ss">:@ident</span><span class="p">,</span> <span class="s2">"instance_variable_get"</span><span class="p">,</span> <span class="p">[</span><span class="mi">11</span><span class="p">,</span> <span class="mi">33</span><span class="p">]],</span>
<span class="p">],</span>
<span class="p">[</span>
<span class="ss">:arg_paren</span><span class="p">,</span>
<span class="p">[</span>
<span class="ss">:args_add_block</span><span class="p">,</span>
<span class="p">[</span>
<span class="p">[</span>
<span class="ss">:symbol_literal</span><span class="p">,</span>
<span class="p">[</span><span class="ss">:symbol</span><span class="p">,</span> <span class="p">[</span><span class="ss">:@ivar</span><span class="p">,</span> <span class="s2">"@attributes"</span><span class="p">,</span> <span class="p">[</span><span class="mi">11</span><span class="p">,</span> <span class="mi">56</span><span class="p">]]],</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="kp">false</span><span class="p">,</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="p">[</span>
<span class="ss">:assign</span><span class="p">,</span>
<span class="p">[</span><span class="ss">:var_field</span><span class="p">,</span> <span class="p">[</span><span class="ss">:@ivar</span><span class="p">,</span> <span class="s2">"@new_record"</span><span class="p">,</span> <span class="p">[</span><span class="mi">12</span><span class="p">,</span> <span class="mi">6</span><span class="p">]]],</span>
<span class="p">[</span><span class="ss">:var_ref</span><span class="p">,</span> <span class="p">[</span><span class="ss">:@kw</span><span class="p">,</span> <span class="s2">"false"</span><span class="p">,</span> <span class="p">[</span><span class="mi">12</span><span class="p">,</span> <span class="mi">20</span><span class="p">]]],</span>
<span class="p">],</span>
<span class="p">[</span>
<span class="ss">:assign</span><span class="p">,</span>
<span class="p">[</span><span class="ss">:var_field</span><span class="p">,</span> <span class="p">[</span><span class="ss">:@ivar</span><span class="p">,</span> <span class="s2">"@previously_new_record"</span><span class="p">,</span> <span class="p">[</span><span class="mi">13</span><span class="p">,</span> <span class="mi">6</span><span class="p">]]],</span>
<span class="p">[</span><span class="ss">:var_ref</span><span class="p">,</span> <span class="p">[</span><span class="ss">:@kw</span><span class="p">,</span> <span class="s2">"false"</span><span class="p">,</span> <span class="p">[</span><span class="mi">13</span><span class="p">,</span> <span class="mi">31</span><span class="p">]]],</span>
<span class="p">],</span>
<span class="p">[</span><span class="ss">:var_ref</span><span class="p">,</span> <span class="p">[</span><span class="ss">:@kw</span><span class="p">,</span> <span class="s2">"self"</span><span class="p">,</span> <span class="p">[</span><span class="mi">14</span><span class="p">,</span> <span class="mi">6</span><span class="p">]]],</span>
<span class="p">],</span>
<span class="kp">nil</span><span class="p">,</span>
<span class="kp">nil</span><span class="p">,</span>
<span class="kp">nil</span><span class="p">,</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="p">],</span>
<span class="p">]</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">failure_message</span>
<span class="c1"># ...</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div><h2 id="the_final_boss">The final boss</h2>
<p>Final, "pragmatic" implementation that I'm sticking with. It depends on <code>parser</code> and <code>method_source</code> gems. I've made peace with them, as they're already in the project via <code>pry</code>, <code>mutant</code> and <code>rubocop</code> additions.</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="nb">require</span> <span class="s2">"parser/current"</span>
<span class="no">RSpec</span><span class="p">.</span><span class="nf">describe</span> <span class="s2">"User"</span> <span class="k">do</span>
<span class="kp">include</span> <span class="no">AST</span><span class="o">::</span><span class="no">Sexp</span>
<span class="n">specify</span> <span class="s2">"#reload method is overridden based on framework implementation"</span> <span class="k">do</span>
<span class="n">expect</span><span class="p">(</span><span class="n">actual_find_record_implementation</span><span class="p">).</span><span class="nf">to</span> <span class="n">eq</span><span class="p">(</span>
<span class="n">expected_find_record_implementation</span><span class="p">,</span>
<span class="p">),</span>
<span class="n">failure_message</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">actual_reload_implementation</span>
<span class="no">Parser</span><span class="o">::</span><span class="no">CurrentRuby</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span>
<span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Persistence</span><span class="p">.</span><span class="nf">instance_method</span><span class="p">(</span><span class="ss">:reload</span><span class="p">).</span><span class="nf">source</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">expected_reload_implementation</span>
<span class="n">s</span><span class="p">(</span>
<span class="ss">:def</span><span class="p">,</span>
<span class="ss">:reload</span><span class="p">,</span>
<span class="n">s</span><span class="p">(</span><span class="ss">:args</span><span class="p">,</span> <span class="n">s</span><span class="p">(</span><span class="ss">:optarg</span><span class="p">,</span> <span class="ss">:options</span><span class="p">,</span> <span class="n">s</span><span class="p">(</span><span class="ss">:nil</span><span class="p">))),</span>
<span class="n">s</span><span class="p">(</span>
<span class="ss">:begin</span><span class="p">,</span>
<span class="n">s</span><span class="p">(</span>
<span class="ss">:send</span><span class="p">,</span>
<span class="n">s</span><span class="p">(</span><span class="ss">:send</span><span class="p">,</span> <span class="n">s</span><span class="p">(</span><span class="ss">:send</span><span class="p">,</span> <span class="n">s</span><span class="p">(</span><span class="ss">:self</span><span class="p">),</span> <span class="ss">:class</span><span class="p">),</span> <span class="ss">:connection</span><span class="p">),</span>
<span class="ss">:clear_query_cache</span><span class="p">,</span>
<span class="p">),</span>
<span class="n">s</span><span class="p">(</span>
<span class="ss">:lvasgn</span><span class="p">,</span>
<span class="ss">:fresh_object</span><span class="p">,</span>
<span class="n">s</span><span class="p">(</span>
<span class="ss">:if</span><span class="p">,</span>
<span class="n">s</span><span class="p">(</span><span class="ss">:send</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="ss">:apply_scoping?</span><span class="p">,</span> <span class="n">s</span><span class="p">(</span><span class="ss">:lvar</span><span class="p">,</span> <span class="ss">:options</span><span class="p">)),</span>
<span class="n">s</span><span class="p">(</span><span class="ss">:send</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="ss">:_find_record</span><span class="p">,</span> <span class="n">s</span><span class="p">(</span><span class="ss">:lvar</span><span class="p">,</span> <span class="ss">:options</span><span class="p">)),</span>
<span class="n">s</span><span class="p">(</span>
<span class="ss">:block</span><span class="p">,</span>
<span class="n">s</span><span class="p">(</span><span class="ss">:send</span><span class="p">,</span> <span class="n">s</span><span class="p">(</span><span class="ss">:send</span><span class="p">,</span> <span class="n">s</span><span class="p">(</span><span class="ss">:self</span><span class="p">),</span> <span class="ss">:class</span><span class="p">),</span> <span class="ss">:unscoped</span><span class="p">),</span>
<span class="n">s</span><span class="p">(</span><span class="ss">:args</span><span class="p">),</span>
<span class="n">s</span><span class="p">(</span><span class="ss">:send</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="ss">:_find_record</span><span class="p">,</span> <span class="n">s</span><span class="p">(</span><span class="ss">:lvar</span><span class="p">,</span> <span class="ss">:options</span><span class="p">)),</span>
<span class="p">),</span>
<span class="p">),</span>
<span class="p">),</span>
<span class="n">s</span><span class="p">(</span>
<span class="ss">:ivasgn</span><span class="p">,</span>
<span class="ss">:@association_cache</span><span class="p">,</span>
<span class="n">s</span><span class="p">(</span>
<span class="ss">:send</span><span class="p">,</span>
<span class="n">s</span><span class="p">(</span><span class="ss">:lvar</span><span class="p">,</span> <span class="ss">:fresh_object</span><span class="p">),</span>
<span class="ss">:instance_variable_get</span><span class="p">,</span>
<span class="n">s</span><span class="p">(</span><span class="ss">:sym</span><span class="p">,</span> <span class="ss">:@association_cache</span><span class="p">),</span>
<span class="p">),</span>
<span class="p">),</span>
<span class="n">s</span><span class="p">(</span>
<span class="ss">:ivasgn</span><span class="p">,</span>
<span class="ss">:@attributes</span><span class="p">,</span>
<span class="n">s</span><span class="p">(</span>
<span class="ss">:send</span><span class="p">,</span>
<span class="n">s</span><span class="p">(</span><span class="ss">:lvar</span><span class="p">,</span> <span class="ss">:fresh_object</span><span class="p">),</span>
<span class="ss">:instance_variable_get</span><span class="p">,</span>
<span class="n">s</span><span class="p">(</span><span class="ss">:sym</span><span class="p">,</span> <span class="ss">:@attributes</span><span class="p">),</span>
<span class="p">),</span>
<span class="p">),</span>
<span class="n">s</span><span class="p">(</span><span class="ss">:ivasgn</span><span class="p">,</span> <span class="ss">:@new_record</span><span class="p">,</span> <span class="n">s</span><span class="p">(</span><span class="ss">:false</span><span class="p">)),</span>
<span class="n">s</span><span class="p">(</span><span class="ss">:ivasgn</span><span class="p">,</span> <span class="ss">:@previously_new_record</span><span class="p">,</span> <span class="n">s</span><span class="p">(</span><span class="ss">:false</span><span class="p">)),</span>
<span class="n">s</span><span class="p">(</span><span class="ss">:self</span><span class="p">),</span>
<span class="p">),</span>
<span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">failure_message</span>
<span class="c1"># ...</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p>As you can see, there are no line or column references in the output. </p>
<p>For the portability, I wish those dependencies weren't needed. Hopefully one day this all will be easier in the future Ruby:</p>
<p><a href="https://twitter.com/_m_b_j_/status/1694830922548257141" target="_blank" rel="noreferrer">
<img src="<%= src_fit("six-ways-mp/tweet-1694830922548257141.png") %>" width="100%" loading="lazy">
</a></p>
<p>Fingers crossed 🤞</p>
tag:mostlyobvio.us,2022-11-17:/2022/11/verifying-content-security-policy-with-selenium-and-cuprite/Verifying Content-Security Policy with Selenium and Cuprite2022-11-17T12:16:10Z2022-11-17T12:16:10Z<h1 id="verifying_content_security_policy_with_selenium_and_cuprite">Verifying Content-Security Policy with Selenium and Cuprite</h1>
<p>Once upon a time, a fellow RailsEventStore enthusiast <a href="https://github.com/RailsEventStore/rails_event_store/issues/1062">reported an issue</a>. It turned out that the <code>RES::Browser</code> component was not compatible with a quite reasonable Content-Security Policy they were using in their Rails app. His report led to an interesting discussion. Eventually, one pull-request later, the project gained new contributor and a more CSP-friendly setup.</p>
<p>How did we ensure that this improvement will not be broken in future releases without manual testing? Read on.</p>
<h2 id="what_is_content_security_policy_">What is Content-Security Policy?</h2>
<p>Quick reminder <a href="https://content-security-policy.com">what this CSP thing is</a>:</p>
<blockquote>
<p>Content-Security-Policy is the name of a HTTP response header that modern browsers use to enhance the security of the document (or web page). The Content-Security-Policy header allows you to restrict how resources such as JavaScript, CSS, or pretty much anything that the browser loads.</p>
</blockquote>
<p>In short — setting CSP headers can <a href="https://edgeguides.rubyonrails.org/security.html#content-security-policy-header">help protect against XSS and injection attacks</a>.</p>
<p>For example, a web server tells your browser, that inline scripts cannot be executed. The web browser knows this because it received the following HTTP header in the response.</p>
<div class="highlight"><pre class="highlight plaintext"><code>content-security-policy: script-src 'self'
</code></pre></div>
<p>Whenever the browser finds an inlined script in the HTML body of the response, it won't execute it.</p>
<div class="highlight"><pre class="highlight html"><code><span class="nt"><script </span><span class="na">type=</span><span class="s">"text/javascript"</span><span class="nt">></span>
<span class="nf">alert</span><span class="p">(</span><span class="dl">"</span><span class="s2">spanish inquisition</span><span class="dl">"</span><span class="p">);</span>
<span class="nt"></script></span>
</code></pre></div>
<p>Instead, an error will be raised and logged. It doesn't matter whether the inlined script was legitimate or injected by the attacker. The policy strictly disallows it.</p>
<div class="highlight"><pre class="highlight plaintext"><code>Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'". Either the 'unsafe-inline' keyword, a hash ('sha256-b1No4u4UwgH6M1mNU7GPc4D3Fc2lJ26AvLJAgCR+lvE='), or a nonce ('nonce-...') is required to enable inline execution.
</code></pre></div><h2 id="how_to_detect_content_security_policy_violation">How to detect Content-Security Policy violation</h2>
<p>At this point, we already know that it's the application or web server dictating policy. And the web browser has "the engine" to verify end enforce it. Thus it would be best to lean on a headless web browser in the test and never look into that black box.</p>
<p>In order to verify desired Content-Security Policy, we first need to emulate it. <code>RES::Browser</code> is technically speaking a Rack application that you either mount in a Rails app or run standalone. Let's focus on the former. That's the most frequent use case.</p>
<p>When mounted, <code>RES::Browser</code> would rely on the CSP header from Rails. In a test, we don't need to involve the whole application though. Just a tiny Rack middleware that adds Content-Security Policy headers will be enough here to emulate it.</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">CspApp</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">app</span><span class="p">,</span> <span class="n">policy</span><span class="p">)</span>
<span class="vi">@app</span> <span class="o">=</span> <span class="n">app</span>
<span class="vi">@policy</span> <span class="o">=</span> <span class="n">policy</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
<span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">response</span> <span class="o">=</span> <span class="vi">@app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
<span class="n">headers</span><span class="p">[</span><span class="s2">"content-security-policy"</span><span class="p">]</span> <span class="o">=</span> <span class="vi">@policy</span>
<span class="p">[</span><span class="n">status</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">response</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p>We will wrap the <code>RES::Browser</code> component with this middleware. Now, when the web browser — driven by <a href="https://github.com/teamcapybara/capybara">Capybara</a> and <a href="https://github.com/rubycdp/cuprite">Cuprite</a> — visits the root URL, it will compare the received Content-Security Policy header with the reality of served HTML. Quickly making objections if there should be any. The same objections would be raised outside the tested system, on a real web browser.</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="n">session</span> <span class="o">=</span>
<span class="no">Capybara</span><span class="o">::</span><span class="no">Session</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span>
<span class="ss">:cuprite</span><span class="p">,</span>
<span class="no">CspApp</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span>
<span class="no">RubyEventStore</span><span class="o">::</span><span class="no">Browser</span><span class="o">::</span><span class="no">App</span><span class="p">.</span><span class="nf">for</span><span class="p">(</span><span class="ss">event_store_locator: </span><span class="o">-></span> <span class="p">{</span> <span class="n">event_store</span> <span class="p">}),</span>
<span class="s2">"style-src 'self'; script-src 'self'"</span><span class="p">,</span>
<span class="p">),</span>
<span class="p">)</span>
<span class="n">session</span><span class="p">.</span><span class="nf">visit</span><span class="p">(</span><span class="s2">"/"</span><span class="p">)</span>
</code></pre></div>
<p>How do we know there were any issues? Parts of the page may not load correctly and we could assert that. That is perfect for checking dynamic content.</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="n">expect</span><span class="p">(</span><span class="n">session</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_content</span><span class="p">(</span><span class="s2">"RubyEventStore v2.5.1"</span><span class="p">)</span>
</code></pre></div>
<p>But what about <a href="https://github.com/RailsEventStore/rails_event_store/issues/1346">inline CSS not loading due to restrictive policy</a>? We may not be able to detect it by looking only at HTML content.
However, more universally — we could peek into web browser logs, looking for errors.</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="n">expect</span><span class="p">(</span><span class="n">logger</span><span class="p">.</span><span class="nf">messages</span><span class="p">.</span><span class="nf">select</span> <span class="p">{</span> <span class="o">|</span><span class="n">m</span><span class="o">|</span> <span class="n">m</span><span class="p">[</span><span class="s2">"params"</span><span class="p">][</span><span class="s2">"entry"</span><span class="p">][</span><span class="s2">"level"</span><span class="p">]</span> <span class="o">==</span> <span class="s2">"error"</span> <span class="p">}).</span><span class="nf">to</span> <span class="n">be_empty</span>
</code></pre></div>
<p>Where does this <code>logger</code> come from? In Cuprite one can pass it to the driver. Logger simply <a href="https://github.com/rubycdp/ferrum#customization">has to respond to puts</a> method. Implementation good enough for a single test might look like this:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="n">logger</span> <span class="o">=</span>
<span class="no">Class</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span>
<span class="nb">attr_reader</span> <span class="ss">:messages</span>
<span class="k">def</span> <span class="nf">initialize</span>
<span class="vi">@messages</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">puts</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
<span class="n">_</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">body</span> <span class="o">=</span> <span class="n">message</span><span class="p">.</span><span class="nf">strip</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="s2">" "</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
<span class="n">body</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">body</span><span class="p">)</span>
<span class="vi">@messages</span> <span class="o"><<</span> <span class="n">body</span> <span class="k">if</span> <span class="n">body</span><span class="p">[</span><span class="s2">"method"</span><span class="p">]</span> <span class="o">==</span> <span class="s2">"Log.entryAdded"</span>
<span class="k">end</span>
<span class="k">end</span><span class="p">.</span><span class="nf">new</span>
<span class="no">Capybara</span><span class="p">.</span><span class="nf">register_driver</span><span class="p">(</span><span class="ss">:cuprite_with_logger</span><span class="p">)</span> <span class="p">{</span> <span class="o">|</span><span class="n">app</span><span class="o">|</span> <span class="no">Capybara</span><span class="o">::</span><span class="no">Cuprite</span><span class="o">::</span><span class="no">Driver</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">app</span><span class="p">,</span> <span class="ss">logger: </span><span class="n">logger</span><span class="p">)</span> <span class="p">}</span>
</code></pre></div><h2 id="cuprite_vs_selenium">Cuprite vs Selenium</h2>
<p>Only recently I've learned that Cuprite does not require Chromedriver to operate. That alone convinced me to give it a try — who doesn't like reducing dependencies? And Chromedriver is this annoying dependency that needs to be frequently updated, in version sync with Chrome browser and <a href="https://timonweb.com/misc/fixing-error-chromedriver-cannot-be-opened-because-the-developer-cannot-be-verified-unable-to-launch-the-chrome-browser-on-mac-os/">lifted from quarantine</a>.</p>
<p>Previously in RailsEventStore were using Selenium with a headless Chrome. On such a setup, we were inspecting browser logs differently. The logger didn't have to be explicitly passed and was already exposed on the driver interface.</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="n">expect</span><span class="p">(</span><span class="n">session</span><span class="p">.</span><span class="nf">driver</span><span class="p">.</span><span class="nf">browser</span><span class="p">.</span><span class="nf">manage</span><span class="p">.</span><span class="nf">logs</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="ss">:browser</span><span class="p">).</span><span class="nf">select</span> <span class="p">{</span> <span class="o">|</span><span class="n">le</span><span class="o">|</span> <span class="n">le</span><span class="p">.</span><span class="nf">level</span> <span class="o">==</span> <span class="s2">"SEVERE"</span> <span class="p">}).</span><span class="nf">to</span> <span class="n">be_empty</span>
</code></pre></div>
<p>Transitioning from Selenium to Cuprite can be best seen fully in this <a href="https://github.com/RailsEventStore/rails_event_store/commit/b6ec85c6cb4510496a4406eef34f3d1111ae9034">commit</a>. I haven't found any drawbacks of Cuprite yet on this small sample set.</p>
<p>Happy hacking!</p>
tag:mostlyobvio.us,2021-05-02:/2021/05/semantic-blind-spot-in-ruby-case-statement/Semantic blind spot in Ruby case statement2021-05-02T17:16:20Z2021-05-02T17:16:20Z<h1 id="semantic_blind_spot_in_ruby_case_statement">Semantic blind spot in Ruby case statement</h1>
<p>Some time ago I've stumbled upon an <a href="https://twitter.com/RubyInside/status/1387015675567353860">article</a> on case statements in Ruby. The author presents there an example of case statement with <a href="https://ruby-doc.org/core-3.0.1/Range.html">ranges</a>:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="k">case</span> <span class="n">number</span>
<span class="k">when</span> <span class="p">(</span><span class="mi">0</span><span class="o">..</span><span class="mi">3</span><span class="p">)</span>
<span class="s1">'low value'</span>
<span class="k">when</span> <span class="p">(</span><span class="mi">4</span><span class="o">..</span><span class="mi">7</span><span class="p">)</span>
<span class="s1">'medium value'</span>
<span class="k">when</span> <span class="p">(</span><span class="mi">8</span><span class="o">..</span><span class="mi">10</span><span class="p">)</span>
<span class="s1">'high value'</span>
<span class="k">else</span>
<span class="s1">'invalid value'</span>
<span class="k">end</span>
</code></pre></div>
<p>The ranges actually read well. I'd even write similar case statement myself. And yet an avid <a href="https://github.com/mbj/mutant">mutant</a> user may tell you there's a "flaw" hidden there. Can you spot it?</p>
<p>Let's pick one branch of that conditional for a closer look. Be it this one:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="k">when</span> <span class="p">(</span><span class="mi">8</span><span class="o">..</span><span class="mi">10</span><span class="p">)</span> <span class="k">then</span> <span class="s1">'high value'</span>
</code></pre></div>
<p>Assume we also have 100% line coverage, reported by <a href="https://github.com/simplecov-ruby/simplecov">simplecov</a> for that example. That's rather easy to achieve:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="k">def</span> <span class="nf">test_high</span>
<span class="n">case_when</span> <span class="o">=</span> <span class="nb">lambda</span> <span class="k">do</span> <span class="o">|</span><span class="n">number</span><span class="o">|</span>
<span class="k">case</span> <span class="n">number</span>
<span class="k">when</span> <span class="p">(</span><span class="mi">0</span><span class="o">..</span><span class="mi">3</span><span class="p">)</span>
<span class="s1">'low value'</span>
<span class="k">when</span> <span class="p">(</span><span class="mi">4</span><span class="o">..</span><span class="mi">7</span><span class="p">)</span>
<span class="s1">'medium value'</span>
<span class="k">when</span> <span class="p">(</span><span class="mi">8</span><span class="o">..</span><span class="mi">10</span><span class="p">)</span>
<span class="s1">'high value'</span>
<span class="k">else</span>
<span class="s1">'invalid value'</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">assert_equal</span> <span class="s1">'high value'</span><span class="p">,</span> <span class="n">case_when</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="mi">8</span><span class="p">)</span>
<span class="n">assert_equal</span> <span class="s1">'high value'</span><span class="p">,</span> <span class="n">case_when</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="mi">9</span><span class="p">)</span>
<span class="n">assert_equal</span> <span class="s1">'high value'</span><span class="p">,</span> <span class="n">case_when</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div>
<p>What would mutant report here, given that 100% line coverage?</p>
<div class="highlight"><pre class="highlight plaintext"><code>Coverage: 88.00%
</code></pre></div>
<p>That drop of the coverage (as mutant sees it) can be attributed to conditions being shadowed by earlier branches. It doesn't really matter if the lower-bound of the range in condition is a bit off. The test still pass.</p>
<div class="highlight"><pre class="highlight plaintext"><code> case number
when (0..3)
"low value"
when (4..7)
"medium value"
- when (8..10)
+ when (1..10)
"high value"
else
"invalid value"
end
</code></pre></div>
<p>The <a href="https://github.com/mbj/mutant">mutant</a> gem is a way to automatically detect such semantic gaps:</p>
<blockquote>
<p>An automated code review tool, with a side effect of producing semantic code coverage metrics.</p>
<p>Think of mutant as an expert developer that simplifies your code while making sure that all tests pass.</p>
</blockquote>
<p>You can perform such code mutations in small scale without mutant — manually. It's a matter of changing the lower-bound in range condition and re-rerunning the tests.</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="k">def</span> <span class="nf">test_high</span>
<span class="n">case_when</span> <span class="o">=</span> <span class="nb">lambda</span> <span class="k">do</span> <span class="o">|</span><span class="n">number</span><span class="o">|</span>
<span class="k">case</span> <span class="n">number</span>
<span class="k">when</span> <span class="p">(</span><span class="mi">0</span><span class="o">..</span><span class="mi">3</span><span class="p">)</span>
<span class="s1">'low value'</span>
<span class="k">when</span> <span class="p">(</span><span class="mi">0</span><span class="o">..</span><span class="mi">7</span><span class="p">)</span>
<span class="s1">'medium value'</span>
<span class="k">when</span> <span class="p">(</span><span class="mi">0</span><span class="o">..</span><span class="mi">10</span><span class="p">)</span>
<span class="s1">'high value'</span>
<span class="k">else</span>
<span class="s1">'invalid value'</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">assert_equal</span> <span class="s1">'high value'</span><span class="p">,</span> <span class="n">case_when</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="mi">8</span><span class="p">)</span>
<span class="n">assert_equal</span> <span class="s1">'high value'</span><span class="p">,</span> <span class="n">case_when</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="mi">9</span><span class="p">)</span>
<span class="n">assert_equal</span> <span class="s1">'high value'</span><span class="p">,</span> <span class="n">case_when</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div>
<p>So what's the semantically reduced case statement, that passes under mutant's scrutiny?</p>
<p>It appears to be this one:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="k">case</span>
<span class="k">when</span> <span class="n">number</span> <span class="o"><</span> <span class="mi">0</span>
<span class="s1">'invalid value'</span>
<span class="k">when</span> <span class="n">number</span> <span class="o"><=</span> <span class="mi">3</span>
<span class="s1">'low value'</span>
<span class="k">when</span> <span class="n">number</span> <span class="o"><=</span> <span class="mi">7</span>
<span class="s1">'medium value'</span>
<span class="k">when</span> <span class="n">number</span> <span class="o"><=</span> <span class="mi">10</span>
<span class="s1">'high value'</span>
<span class="k">else</span>
<span class="s1">'invalid value'</span>
<span class="k">end</span>
</code></pre></div>
<p>Would you call it a good middle ground? Is the case statement still useful in this form?</p>
<p>You can find the code used in this post on my <a href="https://github.com/pawelpacana/case-mutant">github</a>.</p>
<p>Happy mutation testing!</p>
tag:mostlyobvio.us,2021-04-28:/2021/04/rails-console-trick-i-had-no-idea-about/Rails console trick I had no idea about2021-04-28T07:20:25Z2021-04-28T07:20:25Z<h1 id="rails_console_trick_i_had_no_idea_about">Rails console trick I had no idea about</h1>
<p>Tweaking <code>.irbrc</code> to make interactive console comfortable is a highly-rewarding activity. You gets instant boost of productivity and there are less frustrations. There were numerous posts and tips featured in Ruby Weekly on this topic recently.</p>
<p>I've been making my <code>.irbrc</code> more useful too. The harder part was always distributing those changes to remote servers. For example to have those goodies available in Heroku console. And not only for me. There are multiple ways to achieve that, duh. The one that stick though was close to the app code:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># script/likeasir.rb</span>
<span class="c1"># Nice things to have when entering production console</span>
<span class="c1"># load 'script/likeasir.rb'</span>
<span class="k">def</span> <span class="nf">event_store</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">configuration</span><span class="p">.</span><span class="nf">event_store</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">command_bus</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">configuration</span><span class="p">.</span><span class="nf">command_bus</span>
<span class="k">end</span>
<span class="c1"># ...</span>
</code></pre></div>
<p>You'd open the console first and load the helpers next to the IRB session with:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="nb">load</span> <span class="s1">'script/likeasir.rb'</span>
</code></pre></div>
<p>And then <a href="https://blog.arkency.com/authors/jakub-kosinski/">Kuba</a> showed me a neat trick that made this load step completely obsolete:</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="c1"># config/application.rb</span>
<span class="k">module</span> <span class="nn">MyApp</span>
<span class="k">class</span> <span class="nc">Application</span> <span class="o"><</span> <span class="no">Rails</span><span class="o">::</span><span class="no">Application</span>
<span class="c1"># ...</span>
<span class="n">console</span> <span class="k">do</span>
<span class="k">module</span> <span class="nn">DummyConsole</span>
<span class="k">def</span> <span class="nf">event_store</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">configuration</span><span class="p">.</span><span class="nf">event_store</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">command_bus</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">configuration</span><span class="p">.</span><span class="nf">command_bus</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="no">Rails</span><span class="o">::</span><span class="no">ConsoleMethods</span><span class="p">.</span><span class="nf">include</span><span class="p">(</span><span class="no">DummyConsole</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
<p>Now, whenever you load <code>bin/rails c</code>, the <code>command_bus</code> and <code>event_store</code> methods will be present in the IRB session.</p>
<p>That's it. That's the trick I did not know about for years.</p>
<p>You're welcome.</p>