A gotcha of Ruby variable scoping
I recently stumbled over a quirk in the way Ruby handles local variables that I find somewhat dangerous.
Consider:
def salutation(first_name, last_name = nil)
if last_name
full_name = "#{first_name} #{last_name}"
end
"Hi #{full_name}"
end
This is obviously wrong, full_name is unset when last_name is nil.
However, Ruby will not raise an exception. Instead, full_name will simply be nil, and salutation('Bob') returns 'Hi '.
The same would happen in an else branch:
def salutation(fi...
Rails: Example on how to extract domain independent code from the `app/models` folder to the `lib/` folder
This cards describes an example with a Github Client on how to keep your Rails application more maintainable by extracting domain independent code from the app/models folder to the lib/ folder. The approach is applicable to arbitrary scenarios and not limited to API clients.
Example
Let's say we have a Rails application that synchronizes its users with the Github API:
.
└── app
└── models
├── user
│ ├── github_client.rb
│ └── sychronizer.rb
└── user.rb
In this example the app folder ...
Searchkick: async reindexing fails for rails 7 with redis 4
After an upgrade to rails 7 I noticed that async reindexing jobs of Searchkick were failing for Model.reindex(mode: :async, wait: true):
/home/a_user/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/searchkick-5.3.1/lib/searchkick/relation_indexer.rb:142:in `block in batch_job': undefined method `call' for nil (NoMethodError)
Searchkick.with_redis { |r| r.call("SADD", batches_key, [batch_id]) }
^^^^^
from /home/a_user/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/searchkick-5.3.1/lib/searchkick.r...
routing-filter is broken with Rails 7.1
If you are using the routing-filter gem in your Rails 7.1 app for managing URL segments for locales or suffixes, you will notice that the filters do no longer apply, routes are broken and the necessary parameters are no longer extracted. That is because routing-filter patches Rails' find_routes-method to get the current path and apply its defined filters on it. These filters then modify the params that are handed over to your controller action. This way you receive a locale parameter from a ...
Bash script to list git commits by Linear ID
As we're switching from PT to Linear, I've updated the existing bash script to work for commits that are referencing Linear IDs.
A core benefit of our convention to prefix commits by their corresponding issue ID is that we can easily detect commits that belong to the same issue. You can either do that manually or use the bash script below. It can either be placed in your .bashrc or a...
Bookmarklet to generate a commit message for an issue in Linear.app
Your commit messages should include the ID of the issue your code belongs to.
Our preferred syntax prefixes the issue title with its ID in brackets, e.g. [FOO-123] Avatars for users.
Here is how to generate that from an issue in Linear.
Add a new link to your browser's bookmarks bar with the following URL.
javascript:(() => {
if (document.querySelector('[data-view-id="issue-view"]')) {
const [id, ...words] = document.title.split(' ') ;
prompt('Commit message:', `[${id}] ${words.join(' ')}`)
} else {
alert('Open issue...
SAML Single Logout (SLO)
There are two ways a logout in SAML can happen: Service Provider (SP) initiated and Identity Provider (IDP) initiated logout. I'll explain how to implement both flows with devise_saml_authenticatable.
Note
SAML also supports a
SOAPand anArtifactbinding to do this. This guide only refers toPOSTandRedirectbindings.devise_saml_authenticatabledoes not supportSOAPandArtifactbindings.
SP initiated logout (using the Redirect Binding)
When the user clicks on Logout within the app, the app can trigger...
Problems with git submodules in Gitlab CI
If you are using git submodules in Gitlab CI, you might run into a "The project you were looking for could not be found or you don't have permission to view it."
Gitlab added a feature that new projects are no longer allowed to be cloned inside CI runs of other repositories by default. To fix this
- Go into the project used as a submodule
- Go to "Settings" -> "CI/CD" (if you don't see this section, enable it in "Settings" -> "General" -> "Visibility, project features, permissions")
- Go to "Token Access"
- Either disable "Limit access to ...
Heads up: You should always use "current_window.resize_to" to resize the browser window in tests
I recently noticed a new kind of flaky tests on the slow free tier GitHub Action runners: Integration tests were running on smaller screen sizes than specified in the device metrics. The root cause was the use of Selenium's page.driver.resize_window_to methods, which by design does not block until the resizing process has settled:
We discussed this issue again recent...
CI Template for GitHub Actions
Usually our code lives on GitLab, therefore our documentation for CI testing is extensive in this environment. If you are tied to GitHub e.g. because your customer uses it, you may use the following GitHub Actions template for the CI integration. It includes jobs for rspec (parallelized using knapsack, unit + feature specs), rubocop, eslint, coverage and license_finder.
Note that GitHub does not allow the use of YAML anchors and aliases. You can instead use [compos...
Where to keep project files that should not go to Git
Sometimes you have a file that is related to a project, while not actually being part of it. You'd like to keep them around, but others won't need them – e.g. some notes, a log, or a database dump.
Sure, you have a project directory – but all of it is tracked by Git. A project's tmp/ directory is usually not tracked, but by definition it is not a good place to keep things.
An excluded directory for related files
I suggest you keep your related files in a related-files/ directory within your project(s).
To keep this directory u...
Best practices: Writing a Rails script (and how to test it)
A Rails script lives in lib/scripts and is run with bin/rails runner lib/scripts/.... They are a simple tool to perform some one-time actions on your Rails application. A Rails script has a few advantages over pasting some prepared code into a Rails console:
- Version control
- Part of the repository, so you can build on previous scripts for a similar task
- You can have tests (see below)
Although not part of the application, your script is code and should adhere to the common quality standards (e.g. no spaghetti code). However, a scri...
Ignore commits when git blaming
You can ignore certain commits when using git blame with the --ignore-revs-file option. This is handy to ignore large rubocop commits or big renamings in your project. You can add and commit a .git-blame-ignore-revs file in your project to track a list of commits that should be ignored.
# a list of commit shas
123...
456...
Use git blame with the --ignore-revs-file option and ignore the SHAs specified in .git-blame-ignore-revs.
git blame --ignore-revs-file .git-blame-ignore-revs
If you want to use this flag by def...
Disable PostgreSQL's Write-Ahead Log to speed up tests
The linked article suggests an interesting way to speed up tests of Rails + Postgres apps:
PostgreSQL allows the creation of “unlogged” tables, which do not record data in the PostgreSQL Write-Ahead Log. This can make the tables faster, but significantly increases the risk of data loss if the database crashes. As a result, this should not be used in production environments. If you would like all created tables to be unlogged in the test environment you can add the following to your...
Gitlab: How to cancel redundant pipelines
In the Gitlab settings the flag Auto-cancel redundant pipelines is enabled by default. This auto-cancels pipelines with jobs that have the interruptible setting set to true (defaults to false e.g. to not cancel deploys by accident).
Consider setting the interruptible flag for test jobs to reduce the load on your runners like in the following example .gitlab-ci.yml:
rubocop:
interruptible: true
script:
- 'bundle exec ruboco...
RSpec: Composing a custom matcher from existing matchers
When you find similar groups of expect calls in your tests, you can improve readability by extracting the group into its own matcher. RSpec makes this easy by allowing matchers to call other matchers.
Example
The following test checks that two variables foo and bar (1) have no lowercase characters and (2) end with an exclamation mark:
expect(foo).to_not match(/[a-z]/)
expect(foo).to end_with('!')
expect(bar).to_not match(/[a-z]/)
expect(bar).to end_with('!')
We can extract the repeated matcher chains into a custom m...
Transfer records to restore database entries (with Marshal)
If you ever need to restore exact records from one database to another, Marshal might come in handy.
Marshal.dump is part of the ruby core and available in all ruby versions without the need to install anything. This serializes complete ruby objects including id, object_id and all internal state.
Marshal.load deserializes a string to an object. A deserialized object cannot be saved to database directly as the the dumped object was not marked dirty, thus rails does not see the need to save it, even if the object is not present in...
Heads Up: Selenium 4 uses a binary to determine the chromedriver
I recently stumbled over a problem that my feature tests broke in CI because of a mismatching chromedriver version.
In this specific project we have a fixed Chromium version in a Debian 12 environment instead of Chrome. The tests however used a recent chrome version instead.
$ chromedriver --version
ChromeDriver 117.0.5938.149 (e3344ddefa12e60436fa28c81cf207c1afb4d0a9-refs/branch-heads/5938@{#1539})
$ chromium --version
Chromium 117.0.5938.149 built on Debian 12.1, running on Debian 12.1
> WARN Selenium [:selenium_manager] The chromed...
How to make sure that manual deploy tasks (scheduled in Pivotal Tracker) are executed on deploy (with Capistrano)
We regularly have tasks that need to be performed around a deploy. Be it to notify operations about changed application behavior, be it to run a little oneline script after the deploy. Most database-related stuff can be handled by migrations, but every once in a while, we have tasks that are much easier to be performed manually.
Writing deploy tasks
Here is how we manage the deploy tasks themselves:
- Deploy tasks are written inside the Pivotal Tracker story description, clearly marked (e.g. with a headline "Deploy task")
- We disting...
Upgrade Rails: Awareness list
Disclaimer
This card is a collection of guides and things to have in mind when upgrading to a specific version. It is not meant to be complete, so please feel free to contribute!
General workflows
Upgrade to Rails 7
- Don't use log level :debug in your production environments
- [Rails 7.1: Take care of...
Advanced plotting in Ruby with Gnuplot
Besides Plotting graphs in Ruby with Gruff, which comes handy for many uses cases, you sometimes might need configuration for more advanced plots, e.g. for academic concerns. Then using Gnuplot, the first academic open source plotting software, might be a good option.
There are several wrappers for Ruby available and I mainly looked at one of the two most frequently used ones, which are [ruby_gnuplot](https://github.com/rdp/ruby_gnuplot...
Split your parallel tests by execution time and keep execution logs up to date
Both knapsack and parallel_tests have the option to split groups by historic execution time. The required logs for this might be outdated since you manually have to update and push them into your repository.
The following card includes an option how you can keep them consistently up to date with no extra effort locally and/or remotely.
How to always split by execution logs
Parallel Tests
The parallel_tests gem has the option flag `--group...
Lightning Talk: Coverage based Test Case Prioritization in Ruby on Rails
For my computer science bachelor's thesis I programmed and evaluated a CLI Test Case Prioritization (TCP) tool for makandra. It has been written as a Ruby Gem and was tested and evaluated against one Ruby on Rails project. This card will summarize and present the research results, the evaluation and the programmed CLI tool.
The code has been published for educational purposes on GitHub. The german bachelor's thesis has also been included for download at the end.
...
Git restore vs. reset for reverting previous revisions
The git doc states on the difference of these two commands:
- git-restore[1] is about restoring files in the working tree from either the index or another commit. This command does not update your branch. The command can also be used to restore files in the index from another commit.
- git-reset[1] is about updating your branch, moving the tip in order to add or remove commits from the branch. This operation changes the commit history.
git reset can also be used to restore th...