セットプチフォッカ

勉強したアウトプット、ときどきフォッカチオ作っていました

Google Meetで相手の画面やプレゼンテーションが見えないときの対処法

相手が見えない...

先日Google Meetでお話をする機会があったのですが、相手のプレゼンテーションやお顔がまったくみえないという事象に出くわしました...。

お相手の別の端末で会議に参加していただいたところ、すべて映っていたところから「これは自分側に問題があるぞ」と切り分けまでできたのですが、その時間では解決できず1時間音声+テキストで会話を続けました😅

終わってみて色々試してみたところ原因がわかったので、メモしておきます。

結論

設定 > 動画 > 受信時の解像度(最高)が「音声のみ」になっていると相手の動画全般が見えない。
「音声のみ」以外の選択肢に変更する

詳細

バージョン

確認したバージョンは以下の通りです。

設定:受信時の解像度(最高)

原因はGoogle Meetの受信時の設定にありました。設定 > 動画 > 受信時の解像度(最高)を確認します。

相手の画面やカメラが見えていない時は、ここの設定が「音声のみ」になっていました。これにより音声しか受信しなくなるので、相手の動画はすべて見えないというカラクリでした。

この設定は次回以降も保存されるそうなので、なにかの拍子にここを「音声のみ」にしてしまっていて、それが今回も引き継がれていたということになります。

終わりに

過去に「送信時の解像度(最高)」を高画質にしようとしたけど、カクツキすぎるので自動に戻したということがあって、おそらくその際に手が滑ったのでしょう...。

ネット上には「発信している側起因で見えない」ケースが結構転がっていますが、「受信する側起因で見えない」ケースはあまりなかったので、どなたかの参考になれば幸いです。

Vue3のReactivity TransformをJestでもコンパイルできるようにする

要約

Reactivity Transformを使ったコンポーネントをテストしていて、それが原因でJestが落ちるときは、@vue/vue-jest@27.0.0-alpha.3以上を導入しよう!

yarn add -D @vue/vue3-jest@latest

環境

※この記事は2022/03/15書いたものです。執筆時点ではReactivity Transform自体がExperimentalかつ、vue-jestのアルファ版でしか対応していないため下記のような対応が必要になりますが、しばらくすればここらへんは意識しなくても良くなるはずです。

  • Vue:3.2.26
  • Jest:27.5.1

前提:Reactivity Transformとは

Vue 3.2.25からExperimentalとして追加されたコンパイラーマクロで、Vueが提供しているReactiveな値を.valueを経由せずに取得することができるようになります。

// 通常のref
import { ref } from 'vue'

const counter = ref(0)
console.log(counter.value) // 0

// Reactivity Transform
const counter = $ref(0)
console.log(counter) // 0

公式ドキュメントでは以下の章で紹介されていて、refだけでなく、computedも$computedとして使用することができます。

vuejs.org

問題:JestがReactivity Transformを解釈できない

今開発しているアプリはWebpackを利用しているので(正確にはWebpacker...殺せっ!)、ドキュメントの通り、明示的に設定に追記することでこの機能を有効化することができます。

vuejs.org

module.exports = {
  test: /\.vue$/,
  loader: 'vue-loader',
  options: {
    reactivityTransform: true
  }
}

これでアプリ実行時はうまくいくのですが、ユニットテストに使用しているJestではWebpackは関係ないため、この構文の解釈ができずにエラーになります。

node:internal/process/promises:265
            triggerUncaughtException(err, true /* fromPromise */);
            ^

[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "ReferenceError: $computed is not defined".] {
  code: 'ERR_UNHANDLED_REJECTION'
}

今回はこれを解消します。

対処:@vue/vue3-jest@27.0.0-alpha.3以上を導入する

yarn add -D @vue/vue3-jest@27.0.0-alpha.4もしくは@vue/vue3-jest@latestを実行します。

2022/03/15時点ではalpha4が最新版なので、こう書いていますが27.0.0-alpha.3以上を導入すればOKです。

これによりテスト実行時のログに、webpackで実行したときと同じようにexperimentalに対するログが出るようになり、Reactivity Transformが正しく解釈されるようになります。

[@vue/ref-transform] Reactivity transform is an experimental feature.
Experimental features may change behavior between patch versions.
It is recommended to pin your vue dependencies to exact versions to avoid breakage.
You can follow the proposal's status at https://github.com/vuejs/rfcs/discussions/369.

解説

Reactivity Transformのみならず、Vueの仕様についてはGitHub Discussionsにて議論されています。

この中でJestへの対応に関する質問がありました。

github.com

VueとAngularのコントリビュータであるCédricさんが「vue3-jest」に新しいオプションを加えたよという話をしています。それがこのPRです。

github.com

READMEにも同様の内容が書いてあるのですが、27.0.0-alpha3以降は以下のオプションでReactivity Transformを解釈できるようになります。

globals: {
  'vue-jest': {
    compilerOptions: {
      refTransform: false
    }
  }
}

デフォルトではこれがtrueだそうで、先に述べた通りalpha3以上を導入すれば自動的にReactivity Transformが解釈できるようになるわけです。

おわりに

これを解消したはいいんですけど、まだ私のJestはエラーを出力しています。やばいですねっ!

【Rails】collection_selectで表示するテキストには、Procを指定できる

事の発端

自分で作ったGem「jp_local_gov」を使って、自作サービスを実装しています。

f:id:ikmbear:20220303082449p:plain

実装にあたって、市区町村名のセレクトボックスが必要でした。

jp_local_govではView Templateに記載の通り、collection_selectで使用する候補を返すことを目的の一つとしたJpLocalGov.allというAPIを実装しているので、一見以下の形式で目的は達成できそうです。

f.collection_select :local_gov_code, JpLocalGov.all, :code, :city

たしかにこれでも動作としては問題ないのですが、cityとしか指定していないので、「市区町村名」しか表示されません。

日本には「都道府県は異なるけど、市区町村名が同じ市区町村」がそこそこ存在します。

JpLocalGov.all.group_by(&:city).select { |k, v| v.size > 1 }.map { |k, v| { k => v.map(&:prefecture) }}
=>
[{"伊達市"=>["北海道", "福島県"]},
 {"松前町"=>["北海道", "愛媛県"]},
 {"森町"=>["北海道", "静岡県"]},
...

そのため、都道府県と市区町村を合わせてテキストに表示したかったので、雑に以下のようにやってみたのですが動かず。

f.collection_select :local_gov_code, JpLocalGov.all, :code, "#{prefecture}#{:city}"

collection_selectで表示するvalueとtextにはProcを指定できる

api.rubyonrails.org

困った時のAPI Dockというわけでチェックしてみると、

The :value_method and :text_method parameters are methods to be called on each member of collection. The return values are used as the value attribute and contents of each

要はcollection_selectの第1引数に指定したオブジェクトが対応できるメソッドを呼び出すことができ、それゆえにProcも呼び出せるようです。

結果として、

f.collection_select :local_gov_code, JpLocalGov.all, :code, ->(lg) { "#{lg.prefecture} #{lg.city}" }

こんな感じにProcを渡してあげると、JpLocaGov.allで取得されたすべての市区町村がイテレーションされてlgにわたり、それぞれの「都道府県名 市区町村名」として表示できるわけです。

f:id:ikmbear:20220303085115p:plain

Formオブジェクトのロケールファイルの定義方法(i18n)

Formオブジェクトを利用した場合の翻訳キーはどれ?

通常DBに紐づくモデル、つまりActiveRecord::Baseを継承したクラスに対応するフォームを作成する場合、次のようなロケールファイルを作成することで、バリデーションメッセージや表示される項目を多言語化することができます。

ja:
  activerecord: # activerecordをキーにする
    attributes:
      user:
        name: 名前
        password: パスワード

一方DBへの保存だけではなく、複雑なユースケースを実装する場合にはFormオブジェクトを作って対処します。例えばフォームから入力されたデータを用いて、メール送信を行うような場合です。

Formオブジェクトは複数のモデルを扱ったり、DB以外の操作が伴うため、基本的にはActiveModel::Modelをincludeした形で実装します。

また、form_withからの送信先判定を行うために、to_modelをオーバーライドし、メインとなるModelクラスに向けることもあります。

...こうなってくると、i18n用のロケールファイルをどうやって書いたらいいのかよくわからなくなってきたので、Railsがどのようにロケールファイルを解釈しているのか調べました。

要約

  • ActiveModelとActiveRecordはそれぞれi18n_scopeを実装していて、これによりロケールファイルのトップレベルの探索キーが決まる。
  • Formオブジェクト側で行っている処理(例:バリデーション)はactivemodelをキーにする
  • to_modelをオーバーライドしてActiveRecord::Baseを継承したモデルを参照している場合、それらの表示に対する翻訳はactiverecordをキーにする

前提

対象バージョン

サンプルケース

モデル:User

class User < ApplicationRecord
end

フォームオブジェクト:UserForm

class UserForm
    include ActiveModel::Model
  include ActiveModel::Attributes

  attr_reader :user

  def initialize(user = User.new, **attributes)
    @user = user
    attributes = default_attributes if attributes.empty?
    super(attributes)
  end

  validates :name, presence: true
  # バリデーションがいっぱい

  attribute :name, :string
  # カラム定義も様々

  def save
    # 実際の処理
  end

  def to_model
    user
  end
end

RailsがModelのi18nを探索する手順

バリデーションメッセージ

バリデーション用のエラーメッセージの組み立ては、ActiveModel::Error.generata_messageで行われています。

https://github.com/rails/rails/blob/main/activemodel/lib/active_model/error.rb#L64

def self.generate_message(attribute, type, base, options) # :nodoc:
      type = options.delete(:message) if options[:message].is_a?(Symbol)
      value = (attribute != :base ? base.read_attribute_for_validation(attribute) : nil)

      options = {
        model: base.model_name.human,
        attribute: base.class.human_attribute_name(attribute, { base: base }),
        value: value,
        object: base
      }.merge!(options)

      if base.class.respond_to?(:i18n_scope)
        i18n_scope = base.class.i18n_scope.to_s
        attribute = attribute.to_s.remove(/\[\d+\]/)

        defaults = base.class.lookup_ancestors.flat_map do |klass|
          [ :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
            :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
        end
        defaults << :"#{i18n_scope}.errors.messages.#{type}"

        catch(:exception) do
          translation = I18n.translate(defaults.first, **options.merge(default: defaults.drop(1), throw: true))
          return translation unless translation.nil?
        end unless options[:message]
      else
        defaults = []
      end

      defaults << :"errors.attributes.#{attribute}.#{type}"
      defaults << :"errors.messages.#{type}"

      key = defaults.shift
      defaults = options.delete(:message) if options[:message]
      options[:default] = defaults

      I18n.translate(key, **options)
    end

https://api.rubyonrails.org/classes/ActiveModel/Errors.html#method-i-generate_message

抜粋するとこんな感じです。

def self.generate_message(attribute, type, base, options) # :nodoc:
    # 1: 対象のクラスについて、i18n_scopeを実行し、キーを取得する。
  if base.class.respond_to?(:i18n_scope)
    i18n_scope = base.class.i18n_scope.to_s
    attribute = attribute.to_s.remove(/\[\d+\]/)

    defaults = base.class.lookup_ancestors.flat_map do |klass|
      [ :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
        :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
    end
    # 省略
  end

  # 2. 取得できた情報でデータを翻訳文を作成し、つっこむ
  key = defaults.shift
  defaults = options.delete(:message) if options[:message]
  options[:default] = defaults

  # 3. 2で作成されたデータで翻訳処理を行う(ロケールに応じたファイルを選択する)
  I18n.translate(key, **options)
end

ここで肝になるのはi18n_scopeというメソッドです。このメソッドにより、activerecordを見にいくのか、activemodelを見にいくのかが決まります。

ではその実装はどうなっているのかというと、ActiveModelの場合、次のようになっています。

# Returns the +i18n_scope+ for the class. Override if you want custom lookup.
def i18n_scope
  :activemodel
end

https://github.com/rails/rails/blob/75a9e1be75769ae633a938d81d51e06852a69ea3/activemodel/lib/active_model/translation.rb#L26

一方で、ActiveRecordでも同じメソッドがオーバーライドされており、次のように実装されています。

# Set the i18n scope to override ActiveModel.
def i18n_scope # :nodoc:
  :activerecord
end

https://github.com/rails/rails/blob/75a9e1be75769ae633a938d81d51e06852a69ea3/activerecord/lib/active_record/translation.rb#L20

これにより、翻訳対象のクラスが

  • ActiveModelの場合は、activemodel
  • ActiveRecordの場合は、activerecord

が翻訳キーとして採用されることになります。

ここで、バリデーションメッセージについては、Formオブジェクト、つまりActiveModelで実装されたものです。そのため、エラーメッセージの翻訳キーはactivemodel始まりになります。

ja:
    activemodel:
        user_form:
            name: 名前
            password: パスワード

カラム(画面表示項目)

カラム(画面表示項目)の翻訳についても先のi18n_scopeで説明がつきます。

form_withでフォームを作成する際に、to_modelによって、ActiveRecordであるUser側にクラス判定が向くため、ActiveRecordi18n_scopeが採用されます。

そのため、以下のようなロケールファイルを記述します。

ja:
    activerecord:
        user:
            name: 名前
            password: パスワード

なおUserモデルに含まれない項目をFormオブジェクトに定義した場合も、翻訳ファイルはactiverecord下に記述することに注意が必要です。


以上Formオブジェクトでの翻訳キーの参照先についての調査でした。

Slapdashを使って、瞬間で Notionにアイデアを追加する

f:id:ikmbear:20220217080548p:plain

Notionは情報を貯めるには便利だけど、開くのは面倒

Notionも「辛い」と言われるくらいに普及してきましたね。英語版だけだったり、無料ユーザーはブロック数に制限があったりした時代が懐かしいです。

f:id:ikmbear:20220217073348p:plain やっぱり1つの情報を複数の表示形式で閲覧することができるのは非常に画期的で、自分もブログネタを管理するのに使用しています(同じDBをボードとカレンダーで表示して、コンテンツの状況と締切を一覧している)。

こうやって溜まった情報を閲覧するにはすごい便利なんですが、問題はちょっとリッチ & ゆるすぎて、新しい情報を登録するためにわざわざNotionを開くのが面倒くさいんですよね。

特にブログ記事みたいなアイデアは、他の作業をしている時にふっと思いつくものなので、Notionを開くことによるコンテキストスイッチが発生するのがネックでした。

そこでSlapdashというコマンドランチャーの機能を使い、Notionへの登録を簡略化したのですが、これが結構よかったのでご紹介したいと思います。

Slapdashの使い方

Slapdashとは

Slapdashは各種Webアプリを統合し、まとめて操作できるランチャーアプリです。

TrelloやAsanaといったプロジェクト管理ツールから、GitHubFigmaといったクリエイティブツールまで、このアプリから一括で検索、追加、編集を行うことができます(※:アプリによって、操作できる範囲は異なります)。

slapdash.com

f:id:ikmbear:20220217073457g:plain

例えばGitHubを連携した場合、GIFのようにどこからでもコマンドパレットを立ち上げ、自分が関与しているリポジトリのIssueを横断して検索することができます(ここからIssueを選択し、登録・編集操作が可能です)。

f:id:ikmbear:20220217073556p:plain

価格についてはBasic、Pro、Teamsの3つのプランがありますが、今回紹介する範囲はBasic、すなわち無料で使える範囲での機能紹介になります(私もBasic会員です)。

f:id:ikmbear:20220217073617g:plain

ランチャー画面とは別に起動画面があり、ここからアプリを連携したり、Spaceと呼ばれる空間にリンクを突っ込んだり、自分専用のコマンドを作成することができます。

Notionと連携する

f:id:ikmbear:20220217073649p:plain

SlapdashはNotionにも対応しており、操作としては

  • ページの新規作成(Not DB)
  • DBに新たにページを追加
  • 既存のページに対してコンテンツを追加

の3つに対応しています。

f:id:ikmbear:20220217073713p:plain

Notionを連携する場合は、Slapdashを起動してから「Apps」を選択し、

f:id:ikmbear:20220217073728p:plain

App Store」からNotionを選択します(私の場合は接続済になっているので表示が異なりますが、新規接続の場合はConnectというボタンが表示されているので、それを選択します)。

f:id:ikmbear:20220217073743p:plain

この接続を行った時点で存在するNotionのページやDBに対して、Slapdashへと編集の許可が降りるようになっています。そのため、接続後に新しくNotionページやDBを追加し、それらに対してSlapdashから操作を行いたい場合は、Notion側でAPIを許容する必要があります(Notion右上のShare → SlapdashをInviteする)。

Slapdashから実行できるNotionの操作

Notion内の検索を行う

f:id:ikmbear:20220217073814g:plain

「Notion」と入力し、「Filter by Notion」を選択することで、Notionのページを横断的に検索することができます。また「Notion」と入力しなくても検索ワードを直接入力してもページやDBにたどり着くことができます。

SlapdashからNotionのページを作成する

f:id:ikmbear:20220217073856p:plain

「Notion」→「Create New Notion Page」と進むことで、単一のページを作成することができます。作成する際には作成場所となる親のページを指定します。

作成する際にページタイトルやコンテンツは指定できません。あくまでページを作成するのみです。

SlapdashからNotionのデータベースにページを追加する

f:id:ikmbear:20220217073919p:plain

「Notion」→「Add to Notion Database」と進み、対象のDBを指定することで、そのDBに新しくページを追加することができます。

紙芝居でイメージをご紹介しましょう🐵🦀

f:id:ikmbear:20220217073934p:plain

例えば私の場合だと、このようなプロパティを持ったブログネタ管理用のDBがあります。ここに新しくページを追加します。

f:id:ikmbear:20220217080735p:plain

対象のDBとして「ブログ」にカーソルを合わせTab or Enterキーを押します。

f:id:ikmbear:20220217075722p:plain

するとこのDBのプロパティとコンテンツを入力するためのフォームが出てきます。各プロパティには、Notionであらかじめ指定しておいたフォーマットに合わせて入力を行うことができます(リストやカレンダーなど)。

f:id:ikmbear:20220217075130p:plain

「Add」を押すと、作成した記事に対するアクションを指定することができます。今回は「Open in Notion」で作成した情報をチェックしてみましょう。

f:id:ikmbear:20220217075145p:plain

先程入力した内容がNotionに追加されました。Markdownも正しく反映されています。

追加したNotionのページにコンテンツを追加する

先ほどはNotionにページを追加する操作でしたが、追加したページに内容を追記することもできます。

f:id:ikmbear:20220217080935p:plain

「Notion」→「Add to Notion Page」と進み、ページを指定することでコンテンツを追加することができます。こちらも紙芝居で動作をみていきましょう。先程追加した「新しいブログ記事」に対して追加を行っていきます。

f:id:ikmbear:20220217075228p:plain

ページを選択すると、コンテンツの入力フォームが表示されます。こちらもMarkdownで内容を記述することができます。

内容を追記したら「Add」を選択し、先程と同じように「Open in Notion」で作成した情報をチェックしてみます。

f:id:ikmbear:20220217075241p:plain

ページの最下部に今追加し内容が追記されました!

Tips:templateを作成する

「このDBにテーブルを追加することがほとんど」「新規作成時はこのステータスで登録する」といった具合に、具体的なケースがある場合はSlapdashのテンプレート機能を利用して効率化することができます。

f:id:ikmbear:20220217080843p:plain

Slapdashから「Create Command」→「Template」と進みます。

f:id:ikmbear:20220217075309p:plain

テンプレートは連携しているサービスに対してそれぞれ作成できるのですが、今回はNotionへのDB追加のテンプレートを作成するため、「Add to Notion Database」を指定します。

f:id:ikmbear:20220217075328p:plain

まずはNotionのテンプレートを指定します。ここで指定した項目がSlapdashからの登録時のデフォルト値になります。

f:id:ikmbear:20220217075350p:plain

Notion側の設定を終えたら、ページをスクロールしテンプレート自体の設定を行います。

Nameにはテンプレートの名前を、その他アイコンや説明を追記します。エイリアスを設定しておくと、Slapdashのコマンドパレットにそのエイリアスを入力することで、テンプレートを呼び出すことができます。

設定が完了したら「Create Command」を押して、実際に試してみましょう!今回はwtというエイリアスを貼ったので、これをキーに呼び出してみます。

f:id:ikmbear:20220217075428g:plain

呼び出した入力画面に、あらかじめ設定した内容が反映されていることが確認できました。

おわりに

というわけでSlapdashを使ったNotionの更新フローの紹介でした。

私はこれを使って、冒頭にも述べた通りブログネタを登録して思いついた時にアイデアを追記したり、やりたいことリストを追加したりしています。

思いついた時に登録することでアイデアを漏らさず、かつどこからでも登録できることで現在のタスクから大きくコンテキストスイッチをすることもなく、快適に生活できています。

SlapdashはNotion以外のツールにも対応していますので、ぜひ試してみてください。

Appraisalを使っている状態でもRubyMineのGUIでRSpecを実行する

きっかけ

github.com

先日開発したGemはActiveRecordへの対応を仕様として盛り込んでいるため、Appraisalを使って、複数verのActiveRecordでテストができるようにしています。

それはそれでいいんですが、この状態でいつものようにRubyMine上からテストを実行しようとすると、Appraisalで指定しているはずのGem(ActiveRecord)がないためにテストが落ちるようになってしまいました(コマンドラインからAppraisal経由でテストをすれば問題なくとおる)。

Testing started at 7:13 ...

An error occurred while loading spec_helper. - Did you mean?
                    rspec ./spec/spec_helper.rb

Failure/Error: require "active_record"

LoadError:
  cannot load such file -- active_record
# ./spec/spec_helper.rb:6:in `require'
# ./spec/spec_helper.rb:6:in `<top (required)>'
Run options: include {:full_description=>/JpLocalGov\.find/}

All examples were filtered out

というわけで今回はAppraisalを経由しても、RubyMine上でテストを実行できるようにしていきます。

TL:DR;

  • Apppraisalを導入する場合、各種コマンドはbundle exec appraisalの形式で呼び出す必要がある。
  • RubyMine標準のRSpec実行はbundle exec rspec …. であるため、Appraisalが導入されていて、そこでインストールされたGemに依存する実装・テストを書いている場合、RubyMine経由でRSpecが実行できなくなる。
  • AppraisalのCLIを経由するscriptを作成し、RubyMineに設定することで、RubyMine経由でもRSpecを個別に実行できるようになる。

前提のおさらい

Appraisal

Appraisalはthoutbot社が管理している、複数のバージョンでのテストを同時に行うためのGemです。

github.com

公式READMEからの拝借になりますが、以下のように指定することで、Railsの3と4の双方の環境でテストを実行することができます。通常のGemfileでは1つのGemに対して、1つのバージョンしか指定できないため、複数環境に対応したい場合に重宝します。

appraise "rails-3" do
  gem "rails", "3.2.14"
end

appraise "rails-4" do
  gem "rails", "4.0.0"
end

Appraisalを使用する場合、各種コマンドは次のように実行することになります。

bundle exec appraisal {実行対象の環境} <実行したいコマンド>

# 上記例で、rails-3のかんきょうでテストしたい場合、
bundle exec appraisal rails-3 rspec

RubyMineでのRSpec実行

f:id:ikmbear:20220209071635p:plain

RubyMineではテスティングフレームワークに関係なく、GUI上でテストの実行ができます。

一括実行と個別実行の双方を行うことができます。

内部的には、各テスティングフレームワークのexampleやname指定のオプションなどを付与しながら、bundle exec rspecを実行しているだけです。

問題点

上記の通り、Apppraisalはbundle exec appraisalの形式で各コマンドを呼び出すことで、指定した環境のGemfileでコマンドを実行することができます。

逆にいえばAppraisalを経由しないと、Appraisalで指定したGemfileは認識されません。冒頭に述べた通り、Appraisalは複数のバージョンでのテストを可能にするためのGemなので、Appraisalを経由しない = テストが実行できない ということです。

RubyMineでのテスト実行はAppraisalを経由しない形式になっているので、結果としてAppraisalを利用すると、テストが実行できないということになります。

対応策

以下の2ステップで対応します。

  1. Appraisal経由でテストを実行するscriptを作成する
  2. 1のscriptをRubyMineに認識させる。

scriptを作成する

github.com

require 'rubygems'
require 'bundler/setup'
require 'appraisal'
require 'appraisal/cli'

begin
 appraisal_name = ''rails-5.0" # this is just an example, use the appraisal that you have installed
  cmd = [appraisal_name, 'rspec'] + ARGV
  Appraisal::CLI.start(cmd)
rescue Appraisal::AppraisalsNotFound => e
  puts e.message
  exit 127
end

AppraisalのIssueを確認していると、同じような問題が起票されていました。

AppraisalにCLIがあるので、それを間接的に呼び出すようです。このコードでは特定のAppraisal環境をしていするようになっていますが、とりあえずすべてのAppraisal環境で実行するように少し修正しました。

require "rubygems"
require "bundler/setup"
require "appraisal"
require "appraisal/cli"

begin
  `bundle exec appraisal list`.split(/\R/) do |appraisal_name|
    cmd = [appraisal_name, "rspec"] + ARGV
    Appraisal::CLI.start(cmd)
  end
rescue Appraisal::AppraisalsNotFound => e
  puts e.message
  exit 127
end

RubyMineにscriptを認識させる

RubyMineのGUI実行は、それぞれ実行の構成を指定することができます。

zenn.dev

f:id:ikmbear:20220209071725p:plain

複数の構成に対して、まとめて設定を行うためには「Edit configuration templates...」を選択します。

f:id:ikmbear:20220209071741p:plain

左側のサイドバーからRSpecを選択し、「Use cutom RSpec runner script」に、1で作成したscriptを設定します。scriptはどこにおいてもいいですが、Appraisalはプロジェクト固有のものだと思うので、私はプロジェクトのbinフォルダにおいています。

これにより、RubyMineがRSpecを実行するときは、このコマンドを実行するようになります。

実行結果

BUNDLE_GEMFILE=/Users/tadokoroikuma/RubymineProjects/jp_local_gov/gemfiles/rails61.gemfile bundle exec rspec

これらの設定でRSpec実行時にAppraisalのCLIが走るようになり、Gemfileを指定した状態でテストが実行できるようになりました。

感想

Appraisalで作成した環境に対してマトリックス実行する分にはこのスクリプトでいいんですが、「ある特定のテストを特定の環境でのみ実行したい」というケースは対応できないので、対応できるようにしておきたいです(スクリプトの引数処理→Appraisal CLIの引数処理の部分だけクリアできれば、ちゃちゃっとできると思うので)。

Conventional CommitでGemのCHANGELOGをコミットから自動作成する

Conventional Commitとは

概要

t_wadaさんが以前Twitterで言及されているのを見かけて、Conventional Commitなるものがあることを知りました。

Conventional Commitとは、以下のようなコミットメッセージの規約を指します。

Conventional Commits の仕様はコミットメッセージのための軽量の規約です。 明示的なコミット履歴を作成するための簡単なルールを提供します。この規則に従うことで自動化ツールの導入を簡単にします。 コミットメッセージで機能追加・修正・破壊的変更などを説明することで、この規約は SemVerと協調動作します。

引用元:https://www.conventionalcommits.org/ja/v1.0.0/

形式

Conventional Commitは以下の形式を取ります。

<型>[任意 スコープ]: <タイトル>

[任意 本文]

[任意 フッター]

型はfeatfixdocsなど、変更を端的に表した単語です。具体的には次のようになります。

feat(subscribe): メール購読解約時のログインを不要にした

ユーザーのストレス軽減のため(適当)

Closes: #79

RubyMineでのサポート

www.conventionalcommits.org

にもある通り、Conventional Commitをサポートするためのツールはかなりたくさんあります。 所詮はコミットメッセージなので、人力で入力してもConventional Commitにはできるのですが、型やフッターを選択肢から選べたり、枠が決まっているので形式を意識せずにメッセージを作れたりするので、あった方が何かと便利です。

このページにもInteliJ用のツールは掲載されているのですが、私は別途「Conventional Commit」というツールを使っています(RubyMineのPluginストアで評価が高いのを選んだだけ)。

plugins.jetbrains.com

Conventional CommitでCHANGELOGを作る

で、ここからが今回の本題で、Conventional CommitからCHANGELOGを作成します。

何故-conventional-commits-を使うのか には、理由の1つに

• 変更履歴 (CHANGELOG) を自動的に生成できます。

とあって、どうやって自動生成できるのかなと思ったんですが、コミットメッセージが規約に則っていることで、別途ツールを使用すれば機械的CHANGELOGが作成できるということのようです。

今回は以前リリースしたGemのCHANGELOGを作成してみることにしました。

conventional-changelog

Conventional Commitから色々やるためのツールが、conventional-changelogとしてまとまっています。

github.com

この中のconventional-changelog-cliを使用してCHANGELOGを作成します。

github.com

インストール

conventional-changelog-cliはnpmなので、まずはインストールします。

リリースのためだけにリポジトリを汚したくないので、グローバルインストールします(グローバルを汚すのはいいんかい)。

$ npm i -g conventional-changelog-cli

コマンド

conventional-changelog コマンドで実行します。helpを見ると分かる通り、結構色々なオプションがあります。

$ conventional-changelog --help

  Generate a changelog from git metadata

  Usage
    conventional-changelog

  Example
    conventional-changelog -i CHANGELOG.md --same-file

  Options
    -i, --infile              Read the CHANGELOG from this file

    -o, --outfile             Write the CHANGELOG to this file
                              If unspecified, it prints to stdout

    -s, --same-file           Outputting to the infile so you don't need to specify the same file as outfile

    -p, --preset              Name of the preset you want to use. Must be one of the following:
                              angular, atom, codemirror, conventionalcommits, ember, eslint, express, jquery or jshint

    -k, --pkg                 A filepath of where your package.json is located
                              Default is the closest package.json from cwd

    -a, --append              Should the newer release be appended to the older release
                              Default: false

    -r, --release-count       How many releases to be generated from the latest
                              If 0, the whole changelog will be regenerated and the outfile will be overwritten
                              Default: 1

    --skip-unstable           If given, unstable tags will be skipped, e.g., x.x.x-alpha.1, x.x.x-rc.2

    -u, --output-unreleased   Output unreleased changelog

    -v, --verbose             Verbose output. Use this for debugging
                              Default: false

    -n, --config              A filepath of your config script
                              Example of a config script: https://github.com/conventional-changelog/conventional-changelog/blob/master/packages/conventional-changelog-cli/test/fixtures/config.js

    -c, --context             A filepath of a json that is used to define template variables
    -l, --lerna-package       Generate a changelog for a specific lerna package (:pkg-name@1.0.0)
    -t, --tag-prefix          Tag prefix to consider when reading the tags
    --commit-path             Generate a changelog scoped to a specific directory

実際に作ってみる

conventional-changelog -p eslint -i CHANGELOG.md -s

今回はESLint形式で、CHANGELOG.mdに対して追記をしていきます(なんでESLintにしたかというと、ESLintが一番よく知っているパッケージだからというだけです、すみません😅)。

このコマンドを実行してできたCHANGELOGがコチラになります。

# [](https://github.com/IkumaTadokoro/jp_local_gov/compare/v0.1.0...v0.) (2022-01-14)

### chore

* Add RSpec Runner for JetBrains IDE ([83d8c86](https://github.com/IkumaTadokoro/jp_local_gov/commit/83d8c8649f69506c9a08acf275960480316eb988))
* Make JpLocalGov::Data::Importer#prefecture_capital? to return true/false ([82daafd](https://github.com/IkumaTadokoro/jp_local_gov/commit/82daafd19b47e52ff2c1c341a243aef7e5dffae4))

### ci

* Add spell checking GitHub Actions Workflow ([fc23bd1](https://github.com/IkumaTadokoro/jp_local_gov/commit/fc23bd1a3c10cc49f8aaf4e869c0062b50255f03))

### docs

* 💅 ([61c6d66](https://github.com/IkumaTadokoro/jp_local_gov/commit/61c6d66a43b75339036656a1b110dec97156177e))
* Add how to use JpLocalGov.valid_code? ([2e14d71](https://github.com/IkumaTadokoro/jp_local_gov/commit/2e14d71c146d823e4bc3fa268da2e3a0c5f7ebe1))
* Add issue template ([9f416a3](https://github.com/IkumaTadokoro/jp_local_gov/commit/9f416a335247c3be5c52e450b914bd73c380092d)), closes [#47](https://github.com/IkumaTadokoro/jp_local_gov/issues/47)

Closing Keyword を使ってIssueをクローズしていると、参照先のIssueも表示されるみたいです。これは結構便利ですね。

PRをもらった場合にどうするか?

そのリポジトリにコミットする人がみんなConventional Commitを使うかというとOSSの場合はそうもいかないと思います。

私も実際にそのケースに出くわしたのですが、「今回は特例で手動でCHANGELOGを追加するか」ということで、特に調べず終わってしまいました。

で、この記事を執筆する過程で調べてみたところ、以下の記述が見つかりました。

www.conventionalcommits.org

いいえ! Git で squash ベースのワークフローを使用する場合は、主要メンテナがマージ時にコミットメッセージをクリーンアップすることができるため、臨時のコミッタには作業負荷がかかりません。 また、これをするための一般的な方法としては、プルリクエストからのコミットを git システムが自動的に squash し、主要メンテナによるマージ時に適切な git コミットメッセージを入力するためのフォームを表示するというものです。

Conventional Commit側の主張は、「メンテナが都度直せばいいじゃん」とのことでした...。他のリポジトリとかどうしているんだろう?それこそRailsのような大きなリポジトリだとなかなか難しいと思うんですが。

感想

というわけでConventional Commitで自動的にCHANGELOGを作る方法でした。

個人的にはコマンド1つでCHANGELOGが完成するので、気楽にupdate作業をできていい感じです。

PRのマージコミットメッセージの件も含めて、他のOSSがどんな感じにCHANGELOGを書いているのかがすごい気になったのですが、残念ながら作成する過程は見えないので、地域rbとかで聞けたら聞いてみようと思います。

余談:もう少しコミットは細かく積もうかな

少しずれるのですが、最近ちょっと前に以下の動画を見た影響もあって、「できるだけコミットはsquashしておこう」と強く意識しすぎていました。

www.youtube.com

そうすると、なかなかコミットが打てなくて作業的には辛いので、

  • 作業時はconventional commitでこまめにコミットを打つ
  • Pushするときにsquashする

みたいなフローにしたほうがいいなと記事を書いていて思いました。