パスワード文字数を60文字にしたら、はてなブログアプリ(Android)でログインできなくなった

ブラウザや、はてなブックマークアプリ(Android)では大丈夫。 あまり見たことのないエラーなので記念に保存した。

とりあえずGoogle Playのレビューに書いてきたけど、いつか解消すると嬉しい。

Java プロジェクトのソースコードの静的解析と構文チェック

目的

  • 未公開記事の発掘
  • 過去に導入されて、どうしてそうなってるか誰も知らない場合がよくあるやつだった

前提

  • 静的解析って何
    • ソフトウェアの性質を区分値や数値で表現する手法
      • 凝集度みたいな抽象的なメトリクス
      • パッケージ数とかクラス数みたいな具体的なメトリクス
  • 構文チェックって何
    • プログラミング言語の仕様は満たしているけど、慣習や作法を無視していたり、プロジェクトの規約を満たさないソースコード記述を発見する手法
      • コンパイルできるけど実行すると確実にエラーになる記述とか
    • 厳密には静的解析と区別できない
  • 何で必要なの
    • 「ソフトウェアが利用者の目的を達成できているかどうか」以外の性質を理解するため
      • 改修コストに影響する
      • 開発サイクルの期間に影響する
      • 既存メンバーの離脱によるコストに影響する
      • 新しく参加したメンバーが開発サイクルに適応できるまでのコストに影響する
    • ソフトウェアの保守性を向上するため
  • 保守性ってなんだっけ
    • モジュール性
      • ある部品の変更が他の部品に影響する度合いのこと
    • 再利用性
      • ある部品を他の部品の構築に利用できる度合いのこと
    • 解析性
      • ある部分の変更がシステムにどれくらい影響するか表す総合的な評価のこと
    • 修正性
      • システムを修正するときの効率のこと
    • 試験性
      • テスト設計のしやすさや、テスト実施のしやすさの度合いのこと
  • 何で保守性が必要なんだっけ
    • 継続的に機能を追加していく開発スタイルに必須だから
    • 追加する、つまり、既存システムを理解できるようでないといけない
    • 継続的、つまり、人の入れ替えが生じても続けられるようでないといけない

ツールの機能

メトリクスを算出する

  • PMD
    • いろいろできる
  • JavaNCSS
    • だいぶ前に更新が止まってる
  • jQAssistant
    • バックエンドデータベースを Neo4J にして Cyper クエリ言語による宣言的なルール定義ができる

パッケージ数やクラス数、凝集度などのメトリクスを算出します。

潜在バグを検出する

コンパイル結果のバイトコードと合わせてソースコードを解析します。 ルールに基づいてコーディングミスを発見します。 (たとえば null を代入した変数にアクセスしてる、など)

不正なコード記述を検出する

ルールに基づいて不正なコード記述を発見します。

SonarQube は?

Code Quality and Security | SonarQube

デプロイしたサービスにソースコードの解析結果とかを蓄積する形態のプロダクトです。

静的解析としてやりたいことはだいたいできる感じになっています。

ユーザーやグループによる認証認可、プロジェクトという管理単位もあるので、プロジェクトごとに用意するより複数のプロジェクトで共有するほうが効率的じゃないかなと思います。

そういう理由でこの説明では紹介してません。

実装例

参考プロジェクトとして spring-petclinic に記述を追加しました。

ビルドツール

Maven

pom.xml は次のように記述します。

  • プラグインの宣言は pluginManagement に集約
    • バージョン番号を properties 要素で管理するのは大変なので pluginManagement にバージョン番号を記述します
  • nohttp-checkstyle
    • spring.io の公開している checkstyle ルールです
    • https とすべきところが http になっていないかチェックします
  • spring-javaformat-checkstyle
    • spring.io の公開している checkstyle ルールです
    • Spring Frameworkソースコード書式に従っているかどうかをチェックします
      • 改行コードを CR+LF にしたがるのであまりお勧めできません
      • 後述する IDE のフォーマッタに任せるほうがいいと思います
  • maven-surefire-plugin の fork 問題
    • 環境によってはクラスローダーの都合で実行時エラーになるため、ワークアラウンドシステムプロパティ jdk.net.URLClassPath.disableClassPathURLCheck を追加します
    • useSystemClassLoader では制御できない部分の問題です
  <build>
    <pluginManagement>
      <plugins>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>3.0.0-M5</version>
        </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-surefire-report-plugin</artifactId>
          <version>3.0.0-M5</version>
        </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-site-plugin</artifactId>
          <version>3.9.1</version>
        </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-project-info-reports-plugin</artifactId>
          <version>3.1.0</version>
        </plugin>
        <plugin>
          <groupId>io.spring.javaformat</groupId>
          <artifactId>spring-javaformat-maven-plugin</artifactId>
          <version>0.0.24</version>
        </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-pmd-plugin</artifactId>
          <version>3.13.0</version>
        </plugin>
        <plugin>
          <groupId>com.github.spotbugs</groupId>
          <artifactId>spotbugs-maven-plugin</artifactId>
          <version>4.0.4</version>
        </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-checkstyle-plugin</artifactId>
          <version>3.1.1</version>
        </plugin>
      </plugins>
    </pluginManagement>

    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-checkstyle-plugin</artifactId>
        <dependencies>
          <dependency>
            <groupId>com.puppycrawl.tools</groupId>
            <artifactId>checkstyle</artifactId>
            <version>[8.32,)</version>
          </dependency>
          <dependency>
            <groupId>io.spring.nohttp</groupId>
            <artifactId>nohttp-checkstyle</artifactId>
            <version>[0.0.4.RELEASE,)</version>
          </dependency>
          <dependency>
            <groupId>io.spring.javaformat</groupId>
            <artifactId>spring-javaformat-checkstyle</artifactId>
            <version>[0.0.24,)</version>
          </dependency>
        </dependencies>
        <executions>
          <execution>
            <id>nohttp-checkstyle-validation</id>
            <phase>validate</phase>
            <configuration>
              <configLocation>src/checkstyle/nohttp-checkstyle.xml</configLocation>
              <suppressionsLocation>src/checkstyle/nohttp-checkstyle-suppressions.xml</suppressionsLocation>
              <encoding>${project.build.sourceEncoding}</encoding>
              <sourceDirectories>${project.basedir}</sourceDirectories>
              <includes>**/*</includes>
              <excludes>**/.git/**/*,**/.idea/**/*,**/target/**/,**/.flattened-pom.xml,**/*.class</excludes>
              <failsOnError>false</failsOnError>
              <failOnViolation>false</failOnViolation>
            </configuration>
            <goals>
              <goal>check</goal>
            </goals>
          </execution>
          <execution>
            <id>javaformat-validation</id>
            <phase>validate</phase>
            <inherited>true</inherited>
            <configuration>
                <configLocation>io/spring/javaformat/checkstyle/checkstyle.xml</configLocation>
                <includeTestSourceDirectory>true</includeTestSourceDirectory>
                <failsOnError>false</failsOnError>
                <failOnViolation>false</failOnViolation>
            </configuration>
            <goals>
              <goal>check</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <configuration>
          <argLine>-Djdk.net.URLClassPath.disableClassPathURLCheck=true</argLine>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.jacoco</groupId>
        <artifactId>jacoco-maven-plugin</artifactId>
        <version>${jacoco.version}</version>
        <executions>
          <execution>
            <goals>
              <goal>prepare-agent</goal>
            </goals>
          </execution>
          <execution>
            <id>report</id>
            <phase>prepare-package</phase>
            <goals>
              <goal>report</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-site-plugin</artifactId>
        <executions>
          <execution>
            <id>attach-descriptor</id>
            <goals>
              <goal>attach-descriptor</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-pmd-plugin</artifactId>
        <executions>
          <execution>
            <goals>
              <goal>check</goal>
              <goal>cpd-check</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

  <repositories>
    <repository>
      <id>central</id>
      <name>central</name>
      <url>https://repo1.maven.org/maven2/</url>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
      <releases>
        <enabled>true</enabled>
      </releases>
    </repository>
    <repository>
      <id>spring-snapshots</id>
      <name>Spring Snapshots</name>
      <url>https://repo.spring.io/snapshot</url>
      <snapshots>
        <enabled>true</enabled>
      </snapshots>
    </repository>
    <repository>
      <id>spring-milestones</id>
      <name>Spring Milestones</name>
      <url>https://repo.spring.io/milestone</url>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
    </repository>
  </repositories>

  <pluginRepositories>
    <pluginRepository>
      <id>central</id>
      <name>central</name>
      <url>https://repo1.maven.org/maven2/</url>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
      <releases>
        <enabled>true</enabled>
      </releases>
    </pluginRepository>
    <pluginRepository>
      <id>spring-snapshots</id>
      <name>Spring Snapshots</name>
      <url>https://repo.spring.io/snapshot</url>
      <snapshots>
        <enabled>true</enabled>
      </snapshots>
    </pluginRepository>
    <pluginRepository>
      <id>spring-milestones</id>
      <name>Spring Milestones</name>
      <url>https://repo.spring.io/milestone</url>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
    </pluginRepository>
  </pluginRepositories>

  <reporting>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-project-info-reports-plugin</artifactId>
        <reportSets>
          <reportSet>
            <reports>
              <report>index</report>
              <report>licenses</report>
              <report>modules</report>
              <report>dependencies</report>
              <report>plugins</report>
              <report>dependency-info</report>
              <report>dependency-management</report>
              <report>plugin-management</report>
            </reports>
          </reportSet>
        </reportSets>
      </plugin>

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-pmd-plugin</artifactId>
        <configuration>
          <language>java</language>
          <rulesets>
            <ruleset>/category/java/bestpractices.xml</ruleset>
            <ruleset>/category/java/codestyle.xml</ruleset>
            <ruleset>/category/java/design.xml</ruleset>
            <ruleset>/category/java/documentation.xml</ruleset>
            <ruleset>/category/java/errorprone.xml</ruleset>
            <ruleset>/category/java/multithreading.xml</ruleset>
            <ruleset>/category/java/performance.xml</ruleset>
            <ruleset>/category/java/security.xml</ruleset>
          </rulesets>
          <aggregate>true</aggregate>
          <outputDirectory>${project.reporting.outputDirectory}</outputDirectory>
          <targetDirectory>${project.build.directory}</targetDirectory>
          <sourceEncoding>${project.build.sourceEncoding}</sourceEncoding>
          <outputEncoding>${project.reporting.outputEncoding}</outputEncoding>
          <targetJdk>${maven.compiler.source}</targetJdk>
          <failOnViolation>false</failOnViolation>
          <verbose>true</verbose>
          <format>html</format>
        </configuration>
        <reportSets>
          <reportSet>
            <reports>
              <report>pmd</report>
              <report>cpd</report>
            </reports>
          </reportSet>
        </reportSets>
      </plugin>

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-checkstyle-plugin</artifactId>
        <configuration>
          <configLocation>src/checkstyle/google_checks.xml</configLocation>
          <encoding>${project.build.sourceEncoding}</encoding>
          <consoleOutput>false</consoleOutput>
          <failOnViolation>false</failOnViolation>
          <failsOnError>false</failsOnError>
          <linkXRef>false</linkXRef>
        </configuration>
        <reportSets>
          <reportSet>
            <reports>
              <report>checkstyle</report>
            </reports>
          </reportSet>
        </reportSets>
      </plugin>

      <plugin>
        <groupId>com.github.spotbugs</groupId>
        <artifactId>spotbugs-maven-plugin</artifactId>
        <reportSets>
          <reportSet>
            <reports>
              <report>spotbugs</report>
            </reports>
          </reportSet>
        </reportSets>
      </plugin>

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-report-plugin</artifactId>
      </plugin>
    </plugins>
  </reporting>
$ mvn package site
$ ls -l target/site

Gradle

build.gradle は次のように記述します。

plugins {
    id 'java'
    id 'pmd'
    id 'checkstyle'
    id 'com.github.spotbugs' version '4.5.0'
}

pmd {
    ignoreFailures = true
    reportsDir = file("$buildDir/pmd")
    ruleSets = [
        'category/java/bestpractices.xml',
        'category/java/codestyle.xml',
        'category/java/design.xml',
        'category/java/documentation.xml',
        'category/java/errorprone.xml',
        'category/java/multithreading.xml',
        'category/java/performance.xml',
        'category/java/security.xml',
    ]
}

tasks.withType(Pmd) {
    reports {
        html.enabled = true
        xml.enabled = false
    }
}

checkstyle {
    ignoreFailures = true
    reportsDir = file("$buildDir/checkstyle")
    configFile = file('src/checkstyle/google_checks.xml')
    toolVersion = '8.29'
}

tasks.withType(Checkstyle) {
    reports {
        html.enabled = true
        xml.enabled = false
    }
}

spotbugs {
    ignoreFailures = true
    reportsDir = file("$buildDir/spotbugs")
}

tasks.withType(com.github.spotbugs.snom.SpotBugsTask) {
    reports {
        html.enabled = true
        xml.enabled = false
    }
}
$ ./gradlew check
$ ls -l build/pmd build/spotbugs build/checkstyle

IDE

IntelliJ IDEA

以下のようなプラグインを追加しておくと、プロジェクトの設定をしてなくてもビルドするのと同じ感覚で静的解析を実行できます。

IntelliJ の設定ファイルでプラグインを管理する

ガイドを最新に保ち続けるのも面倒なので、必要なプラグインを記述した IntelliJ の設定ファイルをソースコードと一緒にリポジトリへ配置しておくといいでしょう。

.idea/externalDependencies.xml

  • Settings > Build, Execution, Deployment > Required Plugins の設定が格納されています
  • Required Plugins は必須プラグインを管理します
  • 必須プラグインが存在しない状態でプロジェクトをインポートすると、IDE が警告メッセージを表示します
  • Install required plugins をクリックするだけでインストールできます

[./intellij-required-plugin-afford-to-install-plugin.png]

<!-- .idea/externalDependencies.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="ExternalDependencies">
    <plugin id="CheckStyle-IDEA" />
    <plugin id="PMDPlugin" />
    <plugin id="org.jetbrains.plugins.spotbugs" />
  </component>
</project>

Eclipse

もう忘れた

Visual Studio Code

いいのある?

forとforeachは何が同じなのか(未解決→解決)

手癖のようなもので、forではなくforeachを使うほうが多い。 同じものと説明されているけど、どのように同じなのか探してみることにした。

追記

forとforeachは何が同じなのか(未解決) - yujioramaの日記

<a href="https://github.com/Perl/perl5/blob/v5.37.7/toke.c#L8010-L8012" target="_blank" rel="noopener nofollow">https://github.com/Perl/perl5/blob/v5.37.7/toke.c#L8010-L8012</a> ?

2023/01/01 13:02

探してたのはそこでした。ありがとうございます。

字句解析をしてるのはtoke.c。 toke.c#L8010-L8012forforeachを同じ語として扱っている。

// toke.c L8010-L8012
    case KEY_for: 
    case KEY_foreach: 
         return yyl_foreach(aTHX_ s);

yyl_foreachは70行くらいの小さな関数。 後に続く文字列を見て、ポインタを進めるだけだった。

字句解析予約語

予約語を定義してるふうのkeywords.hには定数が並んでいる。まだ別の識別子ということか。

#define KEY_for          70
#define KEY_foreach     71

字句解析をしてるふうのkeywords.cを見ても、forforeachを別々に認識している。 regen/keywords.plが自動生成しているソースコードだけど、語の文字数でswitchして、1文字ずつチェックしているのが面白い。 この時点で、もしかしたら文字数の少ないforのほうがプログラムに優しいのかもしれないと思い始めた。

// keywords.c L304-L310
        case 'f':
          if (name[1] == 'o' &&
              name[2] == 'r')
          {                                       /* for              */
            return KEY_for;
          }

// keywords.c L2082-L2105
        case 'f':
          switch (name[1])
          {
            case 'i':
              if (name[2] == 'n' &&
                  name[3] == 'a' &&
                  name[4] == 'l' &&
                  name[5] == 'l' &&
                  name[6] == 'y')
              {                                   /* finally          */
                return (all_keywords || FEATURE_TRY_IS_ENABLED ? KEY_finally : 0);
              }

              goto unknown;

            case 'o':
              if (name[2] == 'r' &&
                  name[3] == 'e' &&
                  name[4] == 'a' &&
                  name[5] == 'c' &&
                  name[6] == 'h')
              {                                   /* foreach          */
                return KEY_foreach;
              }

構文解析

Perlの構文はperly.yで定義している。bisonを知らなくてもうっすらとPerlソースコードが見えてくる。 この辺forに対応しているようだった。 foreachが見つからない。通り過ぎてしまったのかもしれない。

// perly.y L443-L511
    |   KW_FOR PERLY_PAREN_OPEN remember mnexpr[init_mnexpr] PERLY_SEMICOLON
            { parser->expect = XTERM; }
        texpr PERLY_SEMICOLON
            { parser->expect = XTERM; }
        mintro mnexpr[iterate_mnexpr] PERLY_PAREN_CLOSE
        mblock
            {
              OP *initop = $init_mnexpr;
              OP *forop = newWHILEOP(0, 1, NULL,
                      scalar($texpr), $mblock, $iterate_mnexpr, $mintro);
              if (initop) {
                  forop = op_prepend_elem(OP_LINESEQ, initop,
                  op_append_elem(OP_LINESEQ,
                      newOP(OP_UNSTACK, OPf_SPECIAL),
                      forop));
              }
              PL_hints |= HINT_BLOCK_SCOPE;
              $$ = block_end($remember, forop);
              parser->copline = (line_t)$KW_FOR;
            }
    |   KW_FOR KW_MY remember my_scalar PERLY_PAREN_OPEN mexpr PERLY_PAREN_CLOSE mblock cont
            {
              $$ = block_end($remember, newFOROP(0, $my_scalar, $mexpr, $mblock, $cont));
              parser->copline = (line_t)$KW_FOR;
            }
    |   KW_FOR KW_MY remember PERLY_PAREN_OPEN my_list_of_scalars PERLY_PAREN_CLOSE PERLY_PAREN_OPEN mexpr PERLY_PAREN_CLOSE mblock cont
            {
                          if ($my_list_of_scalars->op_type == OP_PADSV)
                            /* degenerate case of 1 var: for my ($x) ....
                               Flag it so it can be special-cased in newFOROP */
                                $my_list_of_scalars->op_flags |= OPf_PARENS;
              $$ = block_end($remember, newFOROP(0, $my_list_of_scalars, $mexpr, $mblock, $cont));
              parser->copline = (line_t)$KW_FOR;
            }
    |   KW_FOR scalar PERLY_PAREN_OPEN remember mexpr PERLY_PAREN_CLOSE mblock cont
            {
              $$ = block_end($remember, newFOROP(0,
                      op_lvalue($scalar, OP_ENTERLOOP), $mexpr, $mblock, $cont));
              parser->copline = (line_t)$KW_FOR;
            }
    |   KW_FOR my_refgen remember my_var
            { parser->in_my = 0; $<opval>$ = my($my_var); }[variable]
        PERLY_PAREN_OPEN mexpr PERLY_PAREN_CLOSE mblock cont
            {
              $$ = block_end(
                $remember,
                newFOROP(0,
                     op_lvalue(
                        newUNOP(OP_REFGEN, 0,
                            $<opval>variable),
                        OP_ENTERLOOP),
                     $mexpr, $mblock, $cont)
              );
              parser->copline = (line_t)$KW_FOR;
            }
    |   KW_FOR REFGEN refgen_topic PERLY_PAREN_OPEN remember mexpr PERLY_PAREN_CLOSE mblock cont
            {
              $$ = block_end($remember, newFOROP(
                0, op_lvalue(newUNOP(OP_REFGEN, 0,
                             $refgen_topic),
                         OP_ENTERLOOP), $mexpr, $mblock, $cont));
              parser->copline = (line_t)$KW_FOR;
            }
    |   KW_FOR PERLY_PAREN_OPEN remember mexpr PERLY_PAREN_CLOSE mblock cont
            {
              $$ = block_end($remember,
                  newFOROP(0, NULL, $mexpr, $mblock, $cont));
              parser->copline = (line_t)$KW_FOR;
            }

Webブラウザのパスワード管理を見直した

経緯

ブラウザのパスワード管理を統一する

LastPassに登録していたWebサイトを手軽に移動できるならどちらでもよかった。 それぞれのサービスでエクスポート、インポートできる形式が違うのは面白い。

古すぎて消滅したWebサイトのデータがあったりするので、形式を変換しつつ、フィルタリングと並び替えもするスクリプトを作った。見どころは以下です。

  • ヘッダー行の列名を見て読み取り元を判別する
  • 何もマッチしない正規表現 Regexp.union() を知った
  • urlusername の2列で並び替えるため Array#<=> を使った
  • Enumerable#injectCSV#<< を使うと、ヘッダー行ありのCSV形式文字列を簡単に出力できる

password formatter (google account password manager/firefox sync/lastpass)

gist

$ ruby -v
ruby 3.2.0 (2022-12-25 revision a528908271) [x64-mingw-ucrt]

# 構造や形式が違う
$ head -n 1 lastpass.csv firefox.csv chrome.csv
==> lastpass.csv <==
url,username,password,totp,extra,name,grouping,fav

==> firefox.csv <==
"url","username","password","httpRealm","formActionOrigin","guid","timeCreated","timeLastUsed","timePasswordChanged"

==> chrome.csv <==
name,url,username,password

#
# lastpassからchrome
$ ruby password-formatter.rb --to chrome --filter_url=localhost --filter_username=test lastpass.csv > lastpass.chrome.csv
$ head -n 1 lastpass.chrome.csv
"name","url","username","password"

#
# firefoxからchrome
$ ruby password-formatter.rb --to chrome --filter_url=localhost --filter_username=test firefox.csv > firefox.chrome.csv
$ head -n 1 firefox.chrome.csv
"name","url","username","password"

#
# chromeからfirefox
$ ruby password-formatter.rb --to firefox --filter_url=localhost --filter_username=test chrome.csv > chrome.firefox.csv
$ head -n 1 chrome.firefox.csv
"url","username","password","httpRealm","formActionOrigin","guid","timeCreated","timeLastUsed","timePasswordChanged"

Google Account Password Managerは一見すると情報量が少ないんだけど、アプリアカウントも管理していて油断がならない。

  • LastPass
    • ✅テキストファイルをインポートできる
    • ✅テキストファイルをエクスポートできる
    • よくデータ漏洩している
    • やめたい
  • Firefox Sync
    • 同じPCにインストールした別のブラウザのデータをインポートできる
    • ✅テキストファイルをエクスポートできる
    • 知名度が低い
    • Mozilla Foundationには頑張って欲しいので毎月寄付してる
  • Google Account Password Manager
    • ✅テキストファイルをインポートできる
    • ✅テキストファイルをエクスポートできる
    • ほとんどブラウザと一体化してる
    • 近い将来にGoogle Oneを使い始める気がしている

CIの時間をボトムアップに短縮する方策

背景

  • 新機能の追加が継続している
  • 既存機能の改修、改善が継続している
  • 時とともにCIの実行時間が増加し続けている
  • 自動テストはときどき失敗する(flaky)
  • CIの成功が、コードレビューを始めるきっかけになっている(CIが成功するまでコードレビューが始まらない)

問題と原因

  • 問題
    • CIの待ち時間が長く感じる
    • 変更セットと無関係な自動テストがときどき失敗する(再試行すると成功する)
  • 原因
    • CIはすべての自動テストを実行している

解決方法

1. テストの実行を並列化する

  • すべての自動テストを実行すること にこだわりがあるときの戦略
    • プロジェクトによっては、変更セットの正しさを保証する最低限の線引きかもしれない
  • CIやテストフレームワークの仕組みに応じていろいろなやり方がある
    • マシンの並列化
    • プロセスの並列化
    • スレッドの並列化

2. 変更セットに含まれるプログラム部品に依存する自動テストだけを抽出する

  • 差分コンパイルと同じように、変更のあったプログラム部品(コード行、関数、クラス、モジュール、パッケージなどなど)に依存する自動テストが成功すればよい、というところまで、正しさを保証する線を後退させる戦略
  • GitHubソースコードを管理し、GitHub ActionsでCIを実行しているなら次のようなやり方がある

変更セットに含まれるプログラム部品

例えば Perl 言語だと関数単位の差分を上手く計算できないから1、1ファイルに1パッケージという平和協定を信じて git diff --name-only で抽出したファイル名から、パッケージ名を推測するのが無難だと思う。

    steps:
      - id: part of changes
        run: |
          git diff --name-only ${{ github.base_ref }} .. HEAD | grep -v -E '.*\.t' | \
          # "lib/Xxx/Yyy/Zzz.pm"
          sort --dictionary > .part-of-changes

プログラム部品に依存する自動テスト

(もっとスマートなやり方が紹介されているので先に読めばよかった YAPC::NA 2016 Talk)

Perl言語特有の機能を組み合わせると、直接的に依存するテストスクリプトを列挙できる。

requireeval は拾えないけど、CHECK ブロックで %INC を検査すれば、そのテストスクリプトが依存しているパッケージを列挙できる。

こういうパッケージを用意して

package Devel::FindDeps;
use strict;
use warnings;
use utf8;

CHECK {
    my @depends;
    foreach my $module (sort keys %INC) {
        next if $module eq 'Devel/FindDeps.pm';
        push @depends, qq{lib/$module} if -e qq{lib/$module};
    }
    print qq{$0:$_\n} foreach @depends;
    exit;
}

1;

こういうふうに実行すれば テストスクリプト名:依存パッケージ名 の文字列が得られる。

    steps:
      - id: dependent of test
        run: |
          find t -type f -name \*.t | \
          # "t/Xxx/Yyy/Zzz.t"
          xargs --max-args 1 --max-procs 32 perl -Ilib -It/lib -d:FindDeps -c 2>/dev/null | \
          # "t/Xxx/Yyy/Zzz.t:lib/Xxx/Yyy/Zzz.pm"
          sort --dictionary > .dependent-of-test

組み合わせると、変更セットに含まれる部品に依存するテストだけを抽出できる。

    steps:
      - id: dependent of test
        run: |
          find t -type f -name \*.t | \
          # "t/Xxx/Yyy/Zzz.t"
          xargs --max-args 1 --max-procs 32 perl -Ilib -It/lib -d:FindDeps -c 2>/dev/null | \
          # "t/Xxx/Yyy/Zzz.t:lib/Xxx/Yyy/Zzz.pm"
          sort --dictionary > .dependent-of-test
      - id: part of changes
        run: |
          git diff --name-only ${{ github.base_ref }} .. HEAD | grep -v -E '.*\.t' | \
          # "lib/Xxx/Yyy/Zzz.pm"
          sort --dictionary > .part-of-changes
      - id: run dependent test
        run: |
          cat .part-of-changes | \
          xargs --max-args 1 --max-procs 32 grep --no-filename .dependent-of-test | \
          # "t/Xxx/Yyy/Zzz.t:lib/Xxx/Yyy/Zzz.pm"
          cut --delimiter=: --field=1 | \
          # "t/Xxx/Yyy/Zzz.t"
          sort --dictionary --unique | \
          # "t/Xxx/Yyy/Zzz.t"
          prove -lvr --timer --trap -j4

  1. たいていのプログラミング言語git diff --function-context で関数単位の差分を計算できる

自動テストでチェックが失敗したときの説明を見やすくする

これは Perl Advent Calendar 2022 16日目の記事です。

昨日の記事は@hkunoさんのぜんぜんわからない。俺達は雰囲気で perl -p -i.bak をやっている でした。


Test2::Suiteis 関数とTest2::Tools::Compareに登場する比較関数を組み合わせると、ネストしたデータ構造のチェックを(伝統的なスクリプト言語にしては比較的)分かりやすく記述できます。

cpm install Test2::Suite

サンプルコードです。 ステートレスな関数を中心に設計しているなら、道具立てとしては十分でしょう。

このコードの後半に記述したチェックには誤りがあるので、失敗します。 失敗した理由の説明が不格好になってしまうところが本題です。

失敗したチェックの説明が読みにくい

次のように、失敗したチェックは表形式で説明が表示されるのですが、途中で折りたたみが生じていてとても読みにくいのです。

一列目は情報パス(PATH)、二列目は実測値(GOT)、三列目は比較演算子(OP)、四列目は期待値(CHECK)、という、記号を駆使した文字列による表形式です。 行番号(LNs)も付いてるのでだいぶわかりやすいと思います。 ただ、見た目には不満があります。

  • 表の幅が狭くて、それぞれの列幅も狭くなっている
  • 情報パス、実測値、期待値一部の行に折りかえしが生じている(行がずれないように折りかえしてるのは逆にすごい)

私たちのチームにはちゃんと自動テストを作成する文化があるため、日々の開発で感じる不満はできるだけなくしておきたいところです。

表の幅を決めているところを突き止める

テスト結果をTAP(Test Anything Protocol)形式で出力するTest2::Formatter::TAP(Perl5の標準添付モジュール)が、Term::Tableを使用して出力しています。 出力しているのはこの辺、幅を決めているのはこの辺この辺です。

ソースコードを眺めたところ、インストールされているモジュールがあればそれを、無ければ環境変数を、それも無ければ初期値を、という論理になっています。 上記の結果は、初期値の80文字が採用されている状態でした。

まとめ:表の幅を(列の幅を)広げる

普通はログを表示する環境に長い文字列を折りたたむ機能があります。 中途半端に折りたたまれるより、環境変数TABLE_TERM_SIZE=2000 くらい指定して折りたたみが起きないようにするといいでしょう。 列を指定して幅を調整する方法はないのですが、個人的には満足です。 たぶん、GitHub ActionsなどのCIで実行するときも役立ちます。

ちなみに、Term::ReadKeyやTerm::Size::Anyをインストールしても、端末エミュレータの表示幅を使用してくれませんでした。 理由はわからない

TABLE_TERM_SIZE=200 plenv exec prove --nocolor --trap -lvrfomp example.pl
example.pl .. 
# Seeded srand with seed '20221213' from local date.
not ok 1

# Failed test at example.pl line 66.
# +------------------------------+-------------------+----+--------+--------+
# | PATH                         | GOT               | OP | CHECK  | LNs    |
# +------------------------------+-------------------+----+--------+--------+
# |                              | HASH(0x12d819a30) |    | <HASH> | 63, 66 |
# | {abcdefghijklmnopqrstuvwxyz} | def               | eq | defg   | 64     |
# +------------------------------+-------------------+----+--------+--------+
not ok 2

# Failed test at example.pl line 80.
# +-------------------------------------------------+----------------------------+----+--------+--------+
# | PATH                                            | GOT                        | OP | CHECK  | LNs    |
# +-------------------------------------------------+----------------------------+----+--------+--------+
# |                                                 | HASH(0x12e7b80a8)          |    | <HASH> | 77, 80 |
# | {first_field}                                   | HASH(0x12e7bf288)          |    | <HASH> | 78     |
# | {first_field}{second_field}                     | HASH(0x12e7b8060)          |    | <HASH> | 76     |
# | {first_field}{second_field}{third_field}        | HASH(0x12d80aa68)          |    | <HASH> | 74     |
# | {first_field}{second_field}{third_field}{label} | abcdefghijklmnopqrstuvwxyz | == | 124    | 72     |
# +-------------------------------------------------+----------------------------+----+--------+--------+
not ok 3

# Failed test at example.pl line 87.
# +------+--------------------+----+---------+--------+
# | PATH | GOT                | OP | CHECK   | LNs    |
# +------+--------------------+----+---------+--------+
# |      | ARRAY(0x12d819a18) |    | <ARRAY> | 82, 87 |
# | [0]  | 1                  | == | 2       | 83     |
# | [1]  | 2                  | == | 3       | 84     |
# | [2]  | 3                  | == | 4       | 85     |
# +------+--------------------+----+---------+--------+
not ok 4

# Failed test at example.pl line 94.
# +------+--------------------+---------+------------------+--------+
# | PATH | GOT                | OP      | CHECK            | LNs    |
# +------+--------------------+---------+------------------+--------+
# |      | ARRAY(0x12d819a18) |         | <BAG>            | 89, 94 |
# | [*]  | <DOES NOT EXIST>   |         | 4                | 90     |
# | [2]  | 3                  | !exists | <DOES NOT EXIST> |        |
# +------+--------------------+---------+------------------+--------+
not ok 5

# Failed test at example.pl line 102.
# +-------+-----------------------+----+--------------------------------+---------+
# | PATH  | GOT                   | OP | CHECK                          | LNs     |
# +-------+-----------------------+----+--------------------------------+---------+
# |       | Foo=HASH(0x12e7d55f8) |    | <OBJECT>                       | 96, 102 |
# | foo() | 2022                  | eq | abcdefghijklmnopqrstuvwxyz2021 | 98      |
# +-------+-----------------------+----+--------------------------------+---------+
1..5
Dubious, test returned 5 (wstat 1280, 0x500)
Failed 5/5 subtests 

Test Summary Report
-------------------
example.pl (Wstat: 1280 Tests: 5 Failed: 5)
  Failed tests:  1-5
  Non-zero exit status: 5
Files=1, Tests=5,  0 wallclock secs ( 0.01 usr  0.00 sys +  0.03 cusr  0.01 csys =  0.05 CPU)
Result: FAIL

明日の記事は@doikojiさんです。引き続きよろしくお願いします。

SonarQube の Java Rule S2229 を検証する

目的

  • 未公開記事の発掘
  • 3回くらい「動かないんですけど」と相談されたから機械的にチェックしたくて調べてたやつらしい

あるメソッドから同じクラスのメソッドを呼び出すとき、 @Transactionalpropagation 属性に指定した値に間違った組み合わせがあると実行時エラーになる、という問題を警告するルールらしい。

しかし、動かしてみても実行時エラーにはなりませんでした。

いろいろな組み合わせを確認してみた結果は次のとおり。

  • トランザクションを宣言せずに Spring Data JPAリポジトリにアクセスする
  • @Transaction なしのメソッドから @Transaction ありのメソッドにアクセスする
    • 呼び出し先の @Transaction の記述が無視されている
      • require, nested, reuquires_new
    • おそらく Spring Data JPA が介入して Spring Data JPAリポジトリにアクセスに対応するトランザクションが開始する
  • @Transaction ありのメソッドから @Transaction ありのメソッドにアクセスする
    • 呼び出し元の @Transaction に対応するトランザクションは開始している
    • 呼び出し先の @Transaction の記述が無視されている
      • require, nested, reuquires_new

実行時エラーにはなりませんでしたが、呼び出し先の @Transaction の記述は無視されるようです。 NESTED などトランザクション範囲を制御するつもりで使っている場合はちゃんと制御できているか確かめたほうがいいでしょう。

サンプルコードはこちら

@Transaction なしのメソッドから

propagation=NESTED のメソッドを呼び出した場合

DEBUG 5752 o.s.orm.jpa.JpaTransactionManager: Creating new transaction with name [o.s.d.j.r.s.SimpleJpaRepository.deleteAll]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG 5752 o.s.orm.jpa.JpaTransactionManager: Creating new transaction with name [o.s.d.j.r.s.SimpleJpaRepository.save]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG 5752 o.s.orm.jpa.JpaTransactionManager: Creating new transaction with name [o.s.d.j.r.s.SimpleJpaRepository.findAll]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly
DEBUG 5752 o.s.orm.jpa.JpaTransactionManager: Creating new transaction with name [o.s.d.j.r.s.SimpleJpaRepository.save]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG 5752 o.s.orm.jpa.JpaTransactionManager: Creating new transaction with name [o.s.d.j.r.s.SimpleJpaRepository.findAll]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly

propagation=REQUIRED のメソッドを呼び出した場合

DEBUG 5752 o.s.orm.jpa.JpaTransactionManager: Creating new transaction with name [o.s.d.j.r.s.SimpleJpaRepository.deleteAll]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG 5752 o.s.orm.jpa.JpaTransactionManager: Creating new transaction with name [o.s.d.j.r.s.SimpleJpaRepository.save]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG 5752 o.s.orm.jpa.JpaTransactionManager: Creating new transaction with name [o.s.d.j.r.s.SimpleJpaRepository.findAll]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly
DEBUG 5752 o.s.orm.jpa.JpaTransactionManager: Creating new transaction with name [o.s.d.j.r.s.SimpleJpaRepository.save]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG 5752 o.s.orm.jpa.JpaTransactionManager: Creating new transaction with name [o.s.d.j.r.s.SimpleJpaRepository.findAll]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly

propagation=REQUIRE_NEW のメソッドを呼び出した場合

DEBUG 5752 o.s.orm.jpa.JpaTransactionManager: Creating new transaction with name [o.s.d.j.r.s.SimpleJpaRepository.deleteAll]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG 5752 o.s.orm.jpa.JpaTransactionManager: Creating new transaction with name [o.s.d.j.r.s.SimpleJpaRepository.save]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG 5752 o.s.orm.jpa.JpaTransactionManager: Creating new transaction with name [o.s.d.j.r.s.SimpleJpaRepository.findAll]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly
DEBUG 5752 o.s.orm.jpa.JpaTransactionManager: Creating new transaction with name [o.s.d.j.r.s.SimpleJpaRepository.save]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG 5752 o.s.orm.jpa.JpaTransactionManager: Creating new transaction with name [o.s.d.j.r.s.SimpleJpaRepository.findAll]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly

propagation=NESTED のメソッドから

@Transaction なしのメソッドを呼び出した場合

DEBUG 5752 o.s.orm.jpa.JpaTransactionManager: Creating new transaction with name [o.s.d.j.r.s.SimpleJpaRepository.deleteAll]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG 5752 o.s.orm.jpa.JpaTransactionManager: Creating new transaction with name [o.s.d.j.r.s.SimpleJpaRepository.save]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG 5752 o.s.orm.jpa.JpaTransactionManager: Creating new transaction with name [o.b.y.t.i.ExampleServiceImpl.nestedEmpty]: \
  PROPAGATION_NESTED,ISOLATION_DEFAULT

propagation=REQUIRED のメソッドを呼び出した場合

DEBUG 5752 o.s.orm.jpa.JpaTransactionManager: Creating new transaction with name [o.s.d.j.r.s.SimpleJpaRepository.deleteAll]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG 5752 o.s.orm.jpa.JpaTransactionManager: Creating new transaction with name [o.s.d.j.r.s.SimpleJpaRepository.save]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG 5752 o.s.orm.jpa.JpaTransactionManager: Creating new transaction with name [o.b.y.t.i.ExampleServiceImpl.nestedRequire]: \
  PROPAGATION_NESTED,ISOLATION_DEFAULT

propagation=REQUIRE_NEW のメソッドを呼び出した場合

DEBUG 5752 o.s.orm.jpa.JpaTransactionManager: Creating new transaction with name [o.s.d.j.r.s.SimpleJpaRepository.deleteAll]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG 5752 o.s.orm.jpa.JpaTransactionManager: Creating new transaction with name [o.s.d.j.r.s.SimpleJpaRepository.save]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG 5752 o.s.orm.jpa.JpaTransactionManager: Creating new transaction with name [o.b.y.t.i.ExampleServiceImpl.nestedRequireNew]: \
  PROPAGATION_NESTED,ISOLATION_DEFAULT

propagation=REQUIRED のメソッドから

@Transaction なしのメソッドを呼び出した場合

2020-09-03 16:41:49.794 DEBUG 5752 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.deleteAll]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2020-09-03 16:41:49.801 DEBUG 5752 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2020-09-03 16:41:49.807 DEBUG 5752 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.bitbucket.yujiorama.transactionfromsameclass.impl.ExampleServiceImpl.requireEmpty]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT

propagation=NESTED のメソッドを呼び出した場合

2020-09-03 16:41:49.911 DEBUG 5752 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.deleteAll]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2020-09-03 16:41:49.923 DEBUG 5752 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2020-09-03 16:41:49.937 DEBUG 5752 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.bitbucket.yujiorama.transactionfromsameclass.impl.ExampleServiceImpl.requireNested]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT

propagation=REQUIRE_NEW のメソッドを呼び出した場合

2020-09-03 16:41:50.050 DEBUG 5752 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.deleteAll]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2020-09-03 16:41:50.070 DEBUG 5752 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2020-09-03 16:41:50.077 DEBUG 5752 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.bitbucket.yujiorama.transactionfromsameclass.impl.ExampleServiceImpl.requireRequireNew]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT

propagation=REQUIRES_NEW のメソッドから

@Transaction なしのメソッドを呼び出した場合

2020-09-03 16:41:49.693 DEBUG 5752 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.deleteAll]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2020-09-03 16:41:49.733 DEBUG 5752 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2020-09-03 16:41:49.739 DEBUG 5752 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.bitbucket.yujiorama.transactionfromsameclass.impl.ExampleServiceImpl.requireNewEmpty]: \
  PROPAGATION_REQUIRES_NEW,ISOLATION_DEFAULT

propagation=NESTED のメソッドを呼び出した場合

2020-09-03 16:41:49.845 DEBUG 5752 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.deleteAll]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2020-09-03 16:41:49.852 DEBUG 5752 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2020-09-03 16:41:49.857 DEBUG 5752 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.bitbucket.yujiorama.transactionfromsameclass.impl.ExampleServiceImpl.requireNewNested]: \
  PROPAGATION_REQUIRES_NEW,ISOLATION_DEFAULT

propagation=REQUIRED のメソッドを呼び出した場合

2020-09-03 16:41:49.815 DEBUG 5752 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.deleteAll]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2020-09-03 16:41:49.822 DEBUG 5752 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: \
  PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2020-09-03 16:41:49.829 DEBUG 5752 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.bitbucket.yujiorama.transactionfromsameclass.impl.ExampleServiceImpl.requireNewRequire]: \
  PROPAGATION_REQUIRES_NEW,ISOLATION_DEFAULT