日本語
自動テスト
既定のコンソールドライバーは標準入出力のリダイレクトを拒否します。CI や AI コーディング支援による検証には、Prompt.ConsoleDriverFactory で IConsoleDriver を差し替えます。対話端末なしで公開 API とソース生成したバインディングを検証できます。
完全なサンプルを実行する
リポジトリのルートで実行します。
sh
dotnet run --project samples/Sharprompt.Automation -f net8.0
dotnet run --project samples/Sharprompt.Automation -f net10.0Input、Select、MultiSelect、Confirm、Password、List、バリデーション後の再入力、モデルバインディング、開始後のキャンセルを検証します。結果が異なる場合やキー入力が不足した場合は直ちに失敗します。CI でも両フレームワークを実行します。以下のコードはコンパイル対象のサンプルから直接取り込んでいます。
Program.cs
cs
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
using Sharprompt;
using Sharprompt.Automation;
// This program runs without a terminal. Unexpected answers or exhausted input fail the process.
var previousFactory = Prompt.ConsoleDriverFactory;
var previousCancel = Prompt.ThrowExceptionOnCancel;
try
{
Prompt.ThrowExceptionOnCancel = true;
// A prompt owns and disposes its driver. Queue a fresh one for each prompt.
Prompt.ConsoleDriverFactory = () => new ScriptedConsoleDriver().Type("Ada").Enter();
var name = Prompt.Input<string>("Name", validators: [Validators.Required()]);
if (name != "Ada")
{
throw new InvalidOperationException("Unexpected input result.");
}
// No item is highlighted initially: the first Down selects Seattle, the second Tokyo.
Prompt.ConsoleDriverFactory = () => new ScriptedConsoleDriver().Key(ConsoleKey.DownArrow).Key(ConsoleKey.DownArrow).Enter();
var city = Prompt.Select("City", new[] { "Seattle", "Tokyo" });
if (city != "Tokyo")
{
throw new InvalidOperationException("Unexpected selection.");
}
Prompt.ConsoleDriverFactory = () => new ScriptedConsoleDriver().Key(ConsoleKey.DownArrow).Key(ConsoleKey.Spacebar, ' ').Enter();
if (!Prompt.MultiSelect("Cities", new[] { "Tokyo", "London" }).SequenceEqual(new[] { "Tokyo" }))
{
throw new InvalidOperationException("Unexpected multiple selection.");
}
Prompt.ConsoleDriverFactory = () => new ScriptedConsoleDriver().Enter();
if (!Prompt.Confirm("Continue?", defaultValue: true))
{
throw new InvalidOperationException("Unexpected confirmation.");
}
Prompt.ConsoleDriverFactory = () => new ScriptedConsoleDriver().Type("example-password").Enter();
if (Prompt.Password("Password", validators: [Validators.MinLength(8)]) != "example-password")
{
throw new InvalidOperationException("Unexpected password.");
}
Prompt.ConsoleDriverFactory = () => new ScriptedConsoleDriver().Type("42").Enter().Enter();
if (!Prompt.List<int>("Numbers").SequenceEqual(new[] { 42 }))
{
throw new InvalidOperationException("Unexpected list.");
}
// The first Enter fails validation; the same prompt then accepts the scripted text.
Prompt.ConsoleDriverFactory = () => new ScriptedConsoleDriver().Enter().Type("Ada").Enter();
if (Prompt.Input<string>("Required name", validators: [Validators.Required()]) != "Ada")
{
throw new InvalidOperationException("Validation retry failed.");
}
Prompt.ConsoleDriverFactory = () => new ScriptedConsoleDriver().Type("Ada").Enter();
if (Prompt.Bind<Profile>().Name != "Ada")
{
throw new InvalidOperationException("Source-generated binding failed.");
}
// Exercise cancellation after the prompt has started, with no timer or sleep.
using var cancellation = new CancellationTokenSource();
Prompt.ConsoleDriverFactory = () => new ScriptedConsoleDriver { OnRead = cancellation.Cancel };
try
{
Prompt.Input<string>(options => options.Message = "Canceled input", cancellation.Token);
throw new InvalidOperationException("Expected token cancellation.");
}
catch (OperationCanceledException ex) when (ex.CancellationToken == cancellation.Token)
{
}
}
finally
{
Prompt.ConsoleDriverFactory = previousFactory;
Prompt.ThrowExceptionOnCancel = previousCancel;
}
Console.WriteLine("All automation examples passed.");
[PromptBindable]
public class Profile
{
[Required]
public string Name { get; set; } = "";
}ScriptedConsoleDriver.cs
cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using Sharprompt.Drivers;
namespace Sharprompt.Automation;
// A small test driver for the short, ASCII input in this sample. It records rendered text,
// rather than emulating a terminal screen, ANSI styles, Unicode widths, or scrolling.
internal sealed class ScriptedConsoleDriver : IConsoleDriver
{
private readonly Queue<ConsoleKeyInfo> _keys = new();
private readonly StringBuilder _output = new();
public string Output => _output.ToString();
public Action? OnRead { get; init; }
public ScriptedConsoleDriver Key(ConsoleKey key, char keyChar = '\0')
{
_keys.Enqueue(new ConsoleKeyInfo(keyChar, key, false, false, false));
return this;
}
public ScriptedConsoleDriver Type(string text)
{
foreach (var character in text)
{
Key(default, character);
}
return this;
}
public ScriptedConsoleDriver Enter() => Key(ConsoleKey.Enter, '\r');
public ConsoleKeyInfo ReadKey() => ReadKey(CancellationToken.None);
public ConsoleKeyInfo ReadKey(CancellationToken cancellationToken)
{
OnRead?.Invoke();
cancellationToken.ThrowIfCancellationRequested();
// Fail promptly when a prompt unexpectedly needs more input, instead of hanging CI.
return _keys.Count > 0 ? _keys.Dequeue() : throw new InvalidOperationException("Scripted input exhausted.");
}
public void Write(string value, ConsoleColor color)
{
_output.Append(value);
CursorLeft += value.Length;
}
public void WriteLine()
{
_output.Append('\n');
CursorLeft = 0;
CursorTop++;
}
public void SetCursorPosition(int left, int top)
{
CursorLeft = left;
CursorTop = top;
}
public void ClearLine(int top) => SetCursorPosition(0, top);
public void Beep() { }
public void Reset() { }
public void Dispose() { }
// Render after each key. ReadKey(token) is overridden so cancellation never polls here.
public bool KeyAvailable => false;
public bool CursorVisible { set { } }
public int CursorLeft { get; private set; }
public int CursorTop { get; private set; }
public int BufferWidth => 500;
public int BufferHeight => 1000;
public int WindowWidth => 500;
public int WindowHeight => 1000;
public Action CancellationCallback { get; set; } = () => { };
}自分のテストへの組み込み
ConsoleDriverFactoryなどのPrompt設定はプロセス全体で共有されます。変更前の値を保存し、try/finallyで復元します。同じ設定を使うテストは並列実行を無効にしてください。- 各プロンプトはドライバーを所有し、終了時に破棄します。
Bindの各プロパティも含め、毎回新しいドライバーを返します。プロパティごとに入力が異なる場合は、ドライバーのスクリプトをキューで管理します。 - Escape や
CancellationCallbackを使うドライバーのテストではThrowExceptionOnCancel = trueにします。既定ではユーザーのキャンセル操作がプロセスを終了します。トークンによるキャンセルは常にOperationCanceledExceptionです。 - サンプルは
ReadKey(CancellationToken)を実装して待機を避け、入力不足を例外にします。インターフェイスの既定実装を使う場合、KeyAvailableを正確に実装しないと、キャンセル可能なプロンプトが待機し続けることがあります。 - この小さなドライバーは短い ASCII 入力向けです。最終画面ではなく書き込みを記録し、ANSI スタイル、Unicode の表示幅、折り返し、スクロールは再現しません。描画結果の検証には、より完全な端末エミュレーターを使用してください。
README の例の検証
dotnet test -c Release は README のすべての C# ブロックも、標準的なコンソールプロジェクトの暗黙の using を持つ独立プログラムとしてコンパイルします。両ソースジェネレーターも実行します。対話プログラム自体は起動しません。API の変更や、トップレベルステートメントを型宣言の後に置く誤りを検出できます。