パスワード文字数を60文字にしたら、はてなブログアプリ(Android)でログインできなくなった
ブラウザや、はてなブックマークアプリ(Android)では大丈夫。 あまり見たことのないエラーなので記念に保存した。
とりあえずGoogle Playのレビューに書いてきたけど、いつか解消すると嬉しい。
Java プロジェクトのソースコードの静的解析と構文チェック
目的
- 未公開記事の発掘
- 過去に導入されて、どうしてそうなってるか誰も知らない場合がよくあるやつだった
前提
- 静的解析って何
- ソフトウェアの性質を区分値や数値で表現する手法
- 凝集度みたいな抽象的なメトリクス
- パッケージ数とかクラス数みたいな具体的なメトリクス
- ソフトウェアの性質を区分値や数値で表現する手法
- 構文チェックって何
- 何で必要なの
- 「ソフトウェアが利用者の目的を達成できているかどうか」以外の性質を理解するため
- 改修コストに影響する
- 開発サイクルの期間に影響する
- 既存メンバーの離脱によるコストに影響する
- 新しく参加したメンバーが開発サイクルに適応できるまでのコストに影響する
- ソフトウェアの保守性を向上するため
- 「ソフトウェアが利用者の目的を達成できているかどうか」以外の性質を理解するため
- 保守性ってなんだっけ
- モジュール性
- ある部品の変更が他の部品に影響する度合いのこと
- 再利用性
- ある部品を他の部品の構築に利用できる度合いのこと
- 解析性
- ある部分の変更がシステムにどれくらい影響するか表す総合的な評価のこと
- 修正性
- システムを修正するときの効率のこと
- 試験性
- テスト設計のしやすさや、テスト実施のしやすさの度合いのこと
- モジュール性
- 何で保守性が必要なんだっけ
- 継続的に機能を追加していく開発スタイルに必須だから
- 追加する、つまり、既存システムを理解できるようでないといけない
- 継続的、つまり、人の入れ替えが生じても続けられるようでないといけない
ツールの機能
メトリクスを算出する
- PMD
- いろいろできる
- JavaNCSS
- だいぶ前に更新が止まってる
- jQAssistant
- バックエンドデータベースを Neo4J にして Cyper クエリ言語による宣言的なルール定義ができる
パッケージ数やクラス数、凝集度などのメトリクスを算出します。
潜在バグを検出する
コンパイル結果のバイトコードと合わせてソースコードを解析します。
ルールに基づいてコーディングミスを発見します。
(たとえば null
を代入した変数にアクセスしてる、など)
不正なコード記述を検出する
ルールに基づいて不正なコード記述を発見します。
SonarQube は?
Code Quality and Security | SonarQube
デプロイしたサービスにソースコードの解析結果とかを蓄積する形態のプロダクトです。
静的解析としてやりたいことはだいたいできる感じになっています。
ユーザーやグループによる認証認可、プロジェクトという管理単位もあるので、プロジェクトごとに用意するより複数のプロジェクトで共有するほうが効率的じゃないかなと思います。
そういう理由でこの説明では紹介してません。
実装例
参考プロジェクトとして spring-petclinic に記述を追加しました。
ビルドツール
Maven
- maven-pmd-plugin
- javancss-maven-plugin
- jQAssistant
- jQAssistant Java Metris Plugin
- spotbugs-maven-plugin
- maven-checkstyle-plugin
- spring-io/spring-javaformat
- × レポートが出ない
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 問題
<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
- The PMD Plugin
- JavaNCSS
- × Gradle プラグインはない
- jQAssistant
- × Gradle プラグインはない
- github.com/spotbugs/spotbugs-gradle-plugin
- The Checkstyle Plugin
- spring-io/spring-javaformat
- × 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
以下のようなプラグインを追加しておくと、プロジェクトの設定をしてなくてもビルドするのと同じ感覚で静的解析を実行できます。
- PMD
- :first_place: PMDPlugin
- github.com/amitdev/PMD-Intellij
- 個人として開発してる
- アップデートが早い気がする
- :second_place: QAPlug - PMD
- QA Plug
- ベンダーとして開発してる
- CheckStyle や FindBugs を統合した QAPlug の一機能として提供
- アップデートが遅い気がする
- :first_place: PMDPlugin
- jQAssistant
- :first_place: jQAssistant Plugin
- github.com/kontext-e/idea-jqa-plugin
- ベンダーとして開発してる
- :first_place: jQAssistant Plugin
- SpotBugs
- :first_place: SpotBugs
- github.com/JetBrains/spotbugs-intellij-plugin/
- 個人として開発してる。JetBrains がスポンサー
- :first_place: SpotBugs
- CheckStyle
- :first_place: CheckStyle-IDEA
- 個人として開発してる
- アップデートが早い気がする
- :second_place: QAPlug - Checkstyle
- QA Plug
- ベンダーとして開発してる
- CheckStyle や FindBugs を統合した QAPlug の一機能として提供
- アップデートが遅い気がする
- :first_place: CheckStyle-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-L8012でfor
とforeach
を同じ語として扱っている。
// 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を見ても、for
とforeach
を別々に認識している。
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ブラウザのパスワード管理を見直した
経緯
- これまで:個人用途ではいろいろと組み合わせていた
- 最近:仕事の環境は1Passwordで統一されていた
- これから:個人用途も統一しよう
ブラウザのパスワード管理を統一する
LastPassに登録していたWebサイトを手軽に移動できるならどちらでもよかった。 それぞれのサービスでエクスポート、インポートできる形式が違うのは面白い。
古すぎて消滅したWebサイトのデータがあったりするので、形式を変換しつつ、フィルタリングと並び替えもするスクリプトを作った。見どころは以下です。
- ヘッダー行の列名を見て読み取り元を判別する
- 何もマッチしない正規表現 Regexp.union() を知った
url
とusername
の2列で並び替えるため Array#<=> を使ったEnumerable#inject
と CSV#<< を使うと、ヘッダー行ありの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言語特有の機能を組み合わせると、直接的に依存するテストスクリプトを列挙できる。
- %INC / perldoc.jp
do/require,use
したパッケージを%INC
へ格納する
- perlrun / perldoc.jp
- 指定したスクリプトの構文チェックを実行する
perl -c
- 指定したスクリプトの構文チェックを実行する
- BEGIN, UNITCHECK, CHECK, INIT, END / perldoc.jp
CHECK
ブロックは登録した順に、スクリプトを開始する直前に評価する
- perlrun / perldoc.jp
- 任意のパッケージをスクリプトの実行前に読み込ませることができる
perl -d:Foo
でDevel::Foo
を読み込むperl -mFoo
やperl -MFoo
でFoo
を読み込む
require
や eval
は拾えないけど、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
自動テストでチェックが失敗したときの説明を見やすくする
これは Perl Advent Calendar 2022 16日目の記事です。
昨日の記事は@hkunoさんのぜんぜんわからない。俺達は雰囲気で perl -p -i.bak
をやっている でした。
Test2::Suiteの is
関数とTest2::Tools::Compareに登場する比較関数を組み合わせると、ネストしたデータ構造のチェックを(伝統的なスクリプト言語にしては比較的)分かりやすく記述できます。
cpm install Test2::Suite
サンプルコードです。 ステートレスな関数を中心に設計しているなら、道具立てとしては十分でしょう。
このコードの後半に記述したチェックには誤りがあるので、失敗します。 失敗した理由の説明が不格好になってしまうところが本題です。
失敗したチェックの説明が読みにくい
次のように、失敗したチェックは表形式で説明が表示されるのですが、途中で折りたたみが生じていてとても読みにくいのです。
一列目は情報パス(PATH)、二列目は実測値(GOT)、三列目は比較演算子(OP)、四列目は期待値(CHECK)、という、記号を駆使した文字列による表形式です。 行番号(LNs)も付いてるのでだいぶわかりやすいと思います。 ただ、見た目には不満があります。
- 表の幅が狭くて、それぞれの列幅も狭くなっている
- 情報パス、実測値、期待値一部の行に折りかえしが生じている(行がずれないように折りかえしてるのは逆にすごい)
私たちのチームにはちゃんと自動テストを作成する文化があるため、日々の開発で感じる不満はできるだけなくしておきたいところです。
表の幅を決めているところを突き止める
テスト結果をTAP(Test Anything Protocol)形式で出力するTest2::Formatter::TAP(Perl5の標準添付モジュール)が、Term::Tableを使用して出力しています。 出力しているのはこの辺、幅を決めているのはこの辺とこの辺です。
ソースコードを眺めたところ、インストールされているモジュールがあればそれを、無ければ環境変数を、それも無ければ初期値を、という論理になっています。 上記の結果は、初期値の80文字が採用されている状態でした。
- Term::ReadKeyをインストールしている
- 標準入出力から端末エミュレータの幅(文字数)を取得する
- Term::Size::Anyをインストールしている
- 標準入出力から端末エミュレータの幅(文字数)を取得する
- 環境変数
TABLE_TERM_SIZE
を参照する - 初期値の 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回くらい「動かないんですけど」と相談されたから機械的にチェックしたくて調べてたやつらしい
あるメソッドから同じクラスのメソッドを呼び出すとき、 @Transactional
の propagation
属性に指定した値に間違った組み合わせがあると実行時エラーになる、という問題を警告するルールらしい。
しかし、動かしてみても実行時エラーにはなりませんでした。
いろいろな組み合わせを確認してみた結果は次のとおり。
- トランザクションを宣言せずに Spring Data JPA のリポジトリにアクセスする
@Transaction
なしのメソッドから@Transaction
ありのメソッドにアクセスする@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