-
-
Notifications
You must be signed in to change notification settings - Fork 45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[DNM] コマンドの自動テストでストリームしながらキャプチャするためにteeコマンドを使う #458
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
inline comment
@@ -0,0 +1,28 @@ | |||
import Foundation | |||
|
|||
public enum FileUtils { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CartonHelpersにおいてグローバル関数を散らかすのはよくないので、移動するついでにネームスペース型を作ります。
URL(fileURLWithPath: NSTemporaryDirectory()) | ||
} | ||
|
||
public static func makeTemporaryFile(prefix: String, in directory: URL? = nil) throws -> URL { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
carton
モジュールから引っ越しました。
|
||
private let processConcurrentQueue = DispatchQueue( | ||
label: "carton.processConcurrentQueue", | ||
attributes: .concurrent |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
コンカレントキューを使うことで並列性を得ます。
CartonHelpers.Process
のやり方を真似しています。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TSC's Process already has output redirection as a stream.
outputRedirection: OutputRedirection = .collect, startNewProcessGroup: Bool = true, |
Can we just use it?
return lines.joined(separator: "\n") | ||
} | ||
|
||
func checkSuccess() throws { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
他の箇所では nonZeroExit
という言葉が使われていますが、
ProcessResult
という文脈においては、意味的には success
とした方が意味がわかりやすいと思います。
なお、 checkFoo
系のメソッドは 公式APIでは Task.checkCancellation()
などが既存です。
これは Foo を期待し、そうでなければ例外を投げるというコンベンションで、それに合わせています。
|
||
|
||
extension Foundation.Process { | ||
func waitUntilExit() async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
async版を生やします。
} | ||
|
||
func result( | ||
output: Data? = nil, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Foundation.Process
は結果を自分で持てないので、外から渡すインターフェースにしています。
struct SwiftRunProcess { | ||
var process: Foundation.Process | ||
var tee: Foundation.Process | ||
var outputFile: AbsolutePath |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
teeコマンドをパイプで繋げるコンセプトとして型にまとめます。
|
||
func assertZeroExit(_ file: StaticString = #file, line: UInt = #line) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
元々のこのメソッドですが、命名と設計があまり良くないと思います。
まず命名ですが、 assertFoo
は公式では、強い仮定を意味していて、失敗したらクラッシュします。
中身をみれば XCTAssert
系の動作をしますが、
その場合は XCTAssert...
の名前にしないと、公式の assert と曖昧です。
また、ここをテストのアサーションにするのも無意味でしょう。
XCTestのアサーションは、失敗しても次の処理に進んで様々な値をテストするためのものですが、このようなテストでプロセスが失敗した後の続きの処理に意味はほとんどありません。
} | ||
} | ||
|
||
func swiftRunProcess( | ||
_ arguments: [CustomStringConvertible], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Swiftでは余計なexistentialで受ける事はあまりやらないと思います。
暗黙の型変換のように振る舞うので、呼び出し側で型がどうなってるのかわかりづらくなります。
よほどこれが活用されるようなパターンがあるわけでもなかったので、やめます。
["carton", "test", "--environment", "browser", "--port", "8082"], | ||
packageDirectory: packageDirectory.url | ||
) | ||
XCTAssertTrue(result.stdout.contains(expectedContent)) | ||
XCTAssertTrue(try XCTUnwrap(result.utf8Output()).contains(expectedContent)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
キャプチャ結果に対する唯一のテストがこれです。
I personally prefer However, I use In this case, we don't care about build time of test suites, so I prefer |
まず端的に回答します。 そうですね。 また、
はい、僕がこのアプローチを取ったのは、 ただし、これを実現するためには、 carton frontend における また、 frontend は そこまで考えると、今回の実装に テストよりも重要で複雑である driver, plugin, frontend が なお、自前でやっている並行処理といっても、実態はかなり少ないです。 extension Foundation.Process {
func waitUntilExit() async {
await withCheckedContinuation { (continuation) in
processConcurrentQueue.async {
self.waitUntilExit()
continuation.resume()
}
}
}
これは DispatchQueue を使って同期処理を非同期にしただけのパターンです。
public final class Process {
public func waitUntilExit() async throws -> ProcessResult {
try await withCheckedThrowingContinuation { continuation in
DispatchQueue.processConcurrent.async {
self.waitUntilExit(continuation.resume(with:))
}
}
}
}
一方、 また実際のところ、 とはいえ、まだ frontend が 手順としてはまず frontend から |
First of all, Thus I don't like to use
The eventual form in my mind would use the brand new But given that the brand-new API is not going to be available soon, it might be worth using |
なるほど、ありがとうございます。
確かに将来的にはこれがベストなため、 |
#461 でやり直しました。 |
このPRのステータス
#457 に依存しているので、それがマージされるまでは Draft にしておきます。レビューの用意ができました。CIを確認中です。実装を修正中です
課題
テストの
swiftRun
は、プロセスの終了を待ってから、パイプの中身を取り出しています。
これには課題があります。
パイプが詰まるかもしれない
テストでは
swift run carton dev
などのコマンドを実行していますが、これはマシンの状況によっては、
Swift ツールチェーンのダウンロードから、
テスト対象プロジェクトのビルドなどの多くの処理をします。
その間パイプが全くドレインされないので、
OSにバッファされ続けます。
これが最大容量に達すると、
書き込み側プロセスがフリーズするとか、
シグナルによってプロセスがキルされる恐れがあります。
ちなみにLinuxでは容量は 64KB ぐらいらしいので、
which
とかenv
のような小さなコマンドでは問題ない実装ですが、たくさん出力が生じるようなインタラクティブなコマンドでは不適切です。
処理が終わるまで結果が見えない
ツールチェーンのダウンロードなどが生じる場合、
一つのプロセス実行に3分ほどかかる場合があります。
この間全く出力がコンソールに現れないため、
状況がわからないし、
なんらかの理由でプロセスがフリーズしている場合、
ゆっくり進んでいる場合と区別がつかず開発しづらいです。
背景
テストコードでは、ほとんどの場合がステータスコードをチェックしているだけですが、
一件だけプロセスの出力内容に対するテストが書かれています。
このようなプロセス出力に対するテストは有効で、
むしろもっと増やしていくべきでしょう。
提案
課題の解決にはプロセス出力を溜め込まずにストリームするのが簡単ですが、
上述の通りテストでは結果をキャプチャしたいニーズもあります。
そこで
tee
コマンドを使用して、コンソールにストリームしつつ、ファイルにも溜め込みます。
プロセスが終了したらファイルから読み込んで、
内容の検証のニーズに備えます。
実装
メインのプロセスとteeプロセスを繋げて使う場合は、
まずそれぞれのプロセスの停止を待ってから、
その後で出力バッファファイルを読み込みます。
プロセスの停止を待つ処理ですが、
Foundation.waitUntilExit
はブロッキング処理なのが問題です。swift concurrencyと干渉する恐れがあるので、
asyncのインターフェースを提供すべきでしょう。
その後、プロセスの結果を受け取り、ステータスのチェックをします。
実装の簡単さとデバッグしやすさのため、
そのチェックメソッドは失敗すれば例外を投げ、
例外は出力内容をダンプできると良いでしょう。
teeプロセスの書き込み先については、
テンポラリファイルを適切に準備する必要があります。
これは
carton
モジュールに実装があるので、これを
CartonHelpers
モジュールに移動してきて、テストから共用します。
以上の要求に応えるため、
FoundationProcessResult
型を実装します。また、これを握って飛んでいく
FoundationProcessResult.Error
型を実装します。これはほぼ
CartonHelpers.ProcessResult
と同じものですが、CartonHelpers.Process
ではなく、Foundation.Process
と合わせて使うように設計されています。ちなみにですが、
CartonHelpers.ProcessError
という型がProcess+run.swift
で実装されていますが、実は
CartonHelpers.ProcessResult.Error
という型があって、ほぼ重複しています。将来の目標
現在 carton において、
Foudation.Process
とCartonHelpers.Process
という、2つのほぼ同じ役割の型が存在し、利用するコードが混在するのでコードが紛らわしいです。
今回実装した
FoundationProcessResult
とその関連メソッドがあれば、CartonHelpers.Process
でなければできない事はほぼなくなります。将来的には
CartonHelpers.Process
の利用を廃止したいと考えています。