- 投稿日:2020-11-29T23:26:19+09:00
[Azure Functions] 一定確率で少し待ってから応答を返す関数 (C#)
はじめに
LBの動作やアプリ側の挙動確認のため、一定の確率で応答に時間をかけてから返すサービスを作っておきたいなー…と思いたち、Azure Functions で簡単に作ってみました。
環境
- .NET Core 3.1
コード
Visual Studio 2019 のテンプレートから HTTP Trigger で作成。
using System; using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Newtonsoft.Json; namespace Random10SecFunction01 { public static class Function1 { [FunctionName("Random10SecFunction01")] public static async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, ILogger log) { log.LogInformation("C# HTTP trigger function processed a request."); int WAIT_TIME = 10; // seconds Random rnd = new Random(Environment.TickCount); if (rnd.Next(0,99) < 10) { log.LogInformation("Wait..."); await Task.Delay(WAIT_TIME * 1000); } string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); dynamic data = JsonConvert.DeserializeObject(requestBody); string responseMessage = "This HTTP triggered function executed successfully."; return new OkObjectResult(responseMessage); } } }Usage
下記のようにアクセスすると、既定では90%の確率で即時応答、10%の確率で10秒間待ってレスポンスを返します。
<URL>/Random10SecFunction01
- 投稿日:2020-11-29T21:57:22+09:00
【ドメイン駆動設計入門】を読んだ感想
はじめに
私は普段KotlinやJavaを使ってサーバーサイドの開発をしたり、アプリを開発したり、時にはフロントエンドを開発したりしています。
こちらの本を開発の時の設計に一助となればと思い読んでみました。
https://www.amazon.co.jp/dp/479815072X
感想
よかった点
- 「ボトムアップでわかる!」と書いてあるだけあって、 理論先行ではなく具体例を挙げてひとつひとつ説明がありとても読みやすいです。
- 途中で眠くなることなく一気に読み進めることができました。
- DDDにおけるエンティティとクリーンアーキテクチャにおけるエンティティの違いについて言及されていたのがよかったです。このあたりの用語は開発の時に注意しなければと思いました。
よくなかった点
C#
ならではと思う例などもあり、JavaやKotlinではどう変わるだろうか?と思いながら読みました。- 「筆者の見解では」というような記述があり、他の見解も幅広く集めて総合的に理解する必要があると感じました。どんな本でも鵜呑みは危険ですね。
おわりに
設計は本を読んだら急にできるようになるというものではないですね。日々の研鑽を積み重ね少しでも長持ちする設計を作っていきたいです。
- 投稿日:2020-11-29T21:55:38+09:00
UnityのMecanimアニメで、ムービープレーヤーのスライダーのような制御がしたい
やりたいこと
- ムービープレーヤーのスライダーのような制御がしたい
- アニメ全長の任意の位置へジャンプしたいです。
- アニメの任意のフレームを再生したい
- シーケンシャルな再生でなく、任意のフレームに対してランダムにアクセスして、スクリプト制御で正逆任意の速度で再生したいです。
検証環境
- Unity 2019.4.x (LTS)
方法
Animator.Play (int stateNameHash, int layer, float normalizedTime)
を使います。
stateNameHash
に0
を渡すと現在のステートになります。layer
に-1
を渡すと最初に一致したステートになります。normalizedTime
には、正規化された再生位置を渡します。
- ループするアニメの場合は、
0f
と1f
で同じフレームが表示されます。
- その場合は、
0.9999999f
とかにすれば最終フレームになります。- なお…
具体例
以下のコードでは、対象のスライダーとアニメーターが、コードがアタッチされたオブジェクトの子孫の何処かに存在することを前提にしています。
AnimeSlider.csusing UnityEngine; using UnityEngine.UI; public class AnimeSlider : MonoBehaviour { private Slider slider; private Animator animator; /// <summary>初期化</summary> void Start () { slider = GetComponentInChildren<Slider> (); slider.onValueChanged.AddListener (OnChangeSlider); animator = GetComponentInChildren<Animator> (); animator.speed = 0; // 再生をポーズ } /// <summary>スライダが変化</summary> public void OnChangeSlider (float _) { animator.Play (0, -1, slider.normalizedValue); } }
- これによって、スライダーの位置に応じた任意のフレームが再生されます。
- ループするアニメの場合は、スライダの左端と右端で同じフレームが表示されます。
- 同じにしたくない場合は、
slider.normalizedValue * 0.9999999f
とかにします。- ポーズを解除する場合は、
animator.speed = 1;
します。蛇足
複数の同じアニメを時間差で表示
AnimeSlider.csusing UnityEngine; using UnityEngine.UI; public class AnimeSlider : MonoBehaviour { private Slider slider; private Animator [] animators; /// <summary>初期化</summary> void Start () { slider = GetComponentInChildren<Slider> (); slider.onValueChanged.AddListener (OnChangeSlider); animators = GetComponentsInChildren<Animator> (); foreach (var animator in animators) { animator.speed = 0; // 再生をポーズ } } /// <summary>スライダが変化</summary> public void OnChangeSlider (float _) { for (var i = 0; i < animators.Length; i++) { var time = slider.normalizedValue - ((float) i / animators.Length); if (time < 0f) { time += 1f; } animators [i].Play (0, -1, time); } } }
- 投稿日:2020-11-29T21:35:52+09:00
Azure DevOpsでSeleniumを使ったシステムテストがエラーの場合に画面キャプチャを見る方法
Azure DevOpsでSeleniumを使ったシステムテストがエラーの場合に画面キャプチャを見る方法
Seleniumを使ったシステムテストを作った場合、テストがエラーになった場合にその時の画面キャプチャが見られると便利です。特にAzure Devopsで動かした場合、実際の実行状況を見ることが出来ないので、画面キャプチャは重要な情報です。結論から言うとAzure DevOpsの設定は特に必要なくローカルでしかるべき設定をしておけば、それだけでエラーの場合に画面キャプチャを見ることが出来ます。
Seleniumで画面キャプチャを生成する方法
先ず、Seleniumで実行している画面のキャプチャは、Seleniumの機能で撮ることが出来ます。RemoteWebDriverにGetScreenshotというメソッドがあるので、この戻り値のclassのメソッドでSaveAsFileにフルパスのファイル名を指定すれば任意の場所にその画面キャプチャが生成出来ます。
var screenshot = Driver.GetScreenshot(); screenshot.SaveAsFile(@"C:\temp\Capture.jpg");画面キャプチャを欲しいタイミングに限定する方法
この画面キャプチャは全テストで欲しいわけでありません。成功したテストの場合はいらないので、限定するにはテストの結果が分かる必要があります。これはTestContextを使えば取得できます。TestCleanupで、TestContextのCurrentTestOutcomeプロパティを見ればテストの結果も分かるし、またTestNameプロパティでテスト名も取れるので、この名前をファイル名にすれば自動的にユニークに出来ます。ただMSTestでTestContextを使う場合ちょっと癖があります。
TestContextの設定
TestContextのインスタンスをプロパティで1個、staticフィールドで1個、合計2つ持つ必要があります。そして、ClassInitializeで、staticフィールドのTestContextにコピーして、実際に使う時は何も設定しないプロパティのTestContextを使います。プロパティのTestContextを使わずにTestCleanupで_testContextをチェックするとこのテストは何個回していても全て同じテスト名が来てしまいます。何故か何も設定していないプロパティのテスト名は更新されています。staticプロパティ1個だけにすると今度はテストが実行できなくなります。stackoverflowにも書いてありましたが、この2個持ち以外の方法ではうまく行きませんでした。。。
public TestContext TestContext { get; set; } private static TestContext _testContext; [ClassInitialize] public static void ClassInitialize(TestContext testContext) { _testContext = testContext; //TestContext = testContext; //x:フィールドには入れずにプロパティだけに入れると何故かテストが実行できない [TestCleanup] public void TestCelean() { _testContext.CurrentTestOutcome //x:本来こっちで良い気がするが、このインスタンスは最初のテスト情報しか入らない TestContext.CurrentTestOutcome //〇:本来こっちには何も入れてない気がするが、このインスタンスはその時動いたテスト情報が入るテストが成功しなかった時に画面キャプチャを添付する方法
さて、TestContextでテストの結果が取れるようになったので、あとはTestCleanupで、_testContext.CurrentTestOutcomeが成功の場合はreturnしてやります。成功「以外」の場合とすることで、タイムアウトや例外の場合なども対象となります。この状態でTestContextにAddResultFileすればそのファイルを関連画像として扱う事が出来ます。
[TestCleanup] public void TestCelean() { if (TestContext.CurrentTestOutcome == UnitTestOutcome.Passed) return; var screenshot = Driver.GetScreenshot(); var screenshotFile = Path.Combine(Directory.GetCurrentDirectory(), $"{TestContext.TestName}.png"); screenshot.SaveAsFile(screenshotFile); TestContext.AddResultFile(screenshotFile); }これで実際にテストが失敗した時にテストエクスプローラーでみるとこんな感じになります。
Azure DevOpsでエラーの場合に画面キャプチャを見る
この設定をしておけば、Azure DevOpsでテストタスクを実行するだけで、自動的にエラーの場合に画面キャプチャを見ることが出来ます!実際にPipelineで画像をみたい時は、Pipelineの結果画面で、1.Testsをクリック。実際にエラーになったテストが表示されるので、2.テスト名をクリック、3.Attachmentsをクリックすると4にエラーの場合の画面キャプチャが表示されます。便利!
- 投稿日:2020-11-29T20:33:55+09:00
DbContextOptionsBuilderに”UseSqlite”が含まれていませんと言われた時の対処法
ASP.NET Coreのチュートリアルをこなしている最中に以下のようなコンパイルエラーが
発生した。’DbContextOptionsBuilder' does not contain a definition for 'UseSqlite' and no accessible extension method 'UseSqlite' accepting a first argument of type 'DbContextOptionsBuilder' could be found (are you missing a using directive or an assembly reference?)どうやら、'UseSqlite'とやらが’DbContextOptionsBuilder'に含まれていないとのが原因らしい。
対処法
対処方法は単純で、含まれていないのなら、インストールすれば良いとのことで、
ターミナルを開いて、以下のコマンドを実行するれなよい。dotnet add package Microsoft.EntityFrameworkCore.Sqliteこの状態でコンパイルをかけると問題なくコンパイルに成功した。
動作環境
OS : macOS Ver.10.15.17
エディタ:VScode
- 投稿日:2020-11-29T16:37:57+09:00
[C#][WPF]ライフゲームを作ってみるver.1
自分の勉強の為と興味本位でC#(WPF)でライフゲームを作成してみます。
なぜC#で、かつWPFで作るのかと問われると特に意味はありません。何かの縛りプレイだと思っていただくほかないです。
最初はMVVMを守って作ろうかと思ってましたが、DataGridのバインディングがクセ強かったのでとりあえず全部コードビハインドに書いてまずは動かしていきます。ライフゲームとは
セル・オートマトンの一種
以下のルールに従って、生命の誕生、進化、淘汰などのプロセスを簡易的なモデルで再現したシミュレーションゲーム
誕生:死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。
生存:生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。
過疎:生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。
過密:生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。
WPFで作るにあたって
使用するコントロールはDataGridで、各セルを黒(生存)と白(死滅)で塗り分けていきます。
MainWindow.xaml<Window x:Class="WpfTestView.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" ResizeMode="NoResize" Title="MainWindow" Height="650" Width="600" > <Grid Name="parentGridSample"> <StackPanel> <Grid Name="gridSample"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Button Grid.Column="0" Content="Refresh" Click="RefreshButton_Click" /> <Button Grid.Column="1" Content="Random" Click="RandomButton_Click"/> <Button Grid.Column="2" Content="Start" Click="StartButton_Click"/> </Grid> <DataGrid Name="dgSample" HeadersVisibility="None" IsReadOnly="True" SelectionMode="Single" SelectionUnit="Cell" MouseUp="dgSample_MouseUp" SizeChanged="dgSample_SizeChanged"> <DataGrid.Columns> <DataGridTextColumn> <DataGridTextColumn.CellStyle> <Style TargetType="DataGridCell"> <Setter Property="Background" Value="White"/> </Style> </DataGridTextColumn.CellStyle> </DataGridTextColumn> </DataGrid.Columns> </DataGrid> </StackPanel> </Grid> </Window>全てイベントドリブンとして実装、コードビハインドでコントロールを操作しているので全く持ってWinFormsと違いありません。
これが上手く動けばMVVMにリファクタリングしていく予定です。MainWindow.xaml.csusing System; using System.Collections.Generic; using System.Data; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; namespace WpfTestView { /// <summary> /// MainWindow.xaml の相互作用ロジック /// </summary> public partial class MainView : Window { // 内部的なデータテーブル private DataTable _dataTable = new DataTable(); // 次元 private readonly int _rank = 50; // 前世代 private Dictionary<int, List<bool>> preGeneration = new Dictionary<int, List<bool>>(); // 現世代 private Dictionary<int, List<bool>> curGeneration = new Dictionary<int, List<bool>>(); public MainView() { InitializeComponent(); InitializeDataGrid(); InitializeGeneration(); } private void InitializeDataGrid() { dgSample.Columns.Clear(); _dataTable.Rows.Clear(); _dataTable.Columns.Clear(); for (int i = 0; i < _rank; i++) { _dataTable.Columns.Add(); _dataTable.Rows.Add(); } dgSample.ItemsSource = new DataView(_dataTable); ResizeDataGrid(); } private void InitializeGeneration() { preGeneration.Clear(); curGeneration.Clear(); preGeneration = Enumerable.Range(0, _rank).ToDictionary(_ => _, _ => Enumerable.Repeat(false, _rank).ToList()); curGeneration = Enumerable.Range(0, _rank).ToDictionary(_ => _, _ => Enumerable.Repeat(false, _rank).ToList()); } private void ResizeDataGrid() { if (_dataTable.Columns.Count == 0) return; double parentHeghit = parentGridSample.ActualHeight; double parentWidth = parentGridSample.ActualWidth; double buttonHeight = gridSample.ActualHeight; // ボタンを画面上部に付けているのでその分を全体から引いて、行数で均等に割る dgSample.MinRowHeight = 0; dgSample.RowHeight = (parentHeghit - buttonHeight) / _dataTable.Rows.Count; // 何故か親ウィンドウの幅をそのまま割ると少しズレるので-2というマジックナンバーをつけている foreach (var col in dgSample.Columns) { col.MinWidth = 0; col.Width = (parentWidth - 2) / _dataTable.Columns.Count; } } private void dgSample_SizeChanged(object sender, SizeChangedEventArgs e) { Trace.WriteLine(MethodBase.GetCurrentMethod().Name); ResizeDataGrid(); } private void RefreshButton_Click(object sender, RoutedEventArgs e) { Trace.WriteLine(MethodBase.GetCurrentMethod().Name); InitializeDataGrid(); InitializeGeneration(); } private void dgSample_MouseUp(object sender, MouseButtonEventArgs e) { Trace.WriteLine(MethodBase.GetCurrentMethod().Name); // マウスクリックされたセルを取得して色を反転させる var curCellInfo = dgSample.CurrentCell; var curCell = curCellInfo.Column.GetCellContent(curCellInfo.Item).Parent as DataGridCell; if (curCell == null) return; int rowIdx = dgSample.Items.IndexOf(curCellInfo.Item); int colIdx = curCellInfo.Column.DisplayIndex; curGeneration[rowIdx][colIdx] = !curGeneration[rowIdx][colIdx]; ToggleCellColor(curCell); } private void StartButton_Click(object sender, RoutedEventArgs e) { Trace.WriteLine(MethodBase.GetCurrentMethod().Name); // とりあえず100世代ライフゲームを実行する int gen = 0; Task.Factory.StartNew(() => { do { UpdateGeneration(); System.Threading.Thread.Sleep(200); if (curGeneration.All(_n => _n.Value.All(_ => !_))) break; } while (++gen < 100); }); } private void UpdateGeneration() { for (int i = 0; i < _rank; i++) { for (int j = 0; j < _rank; j++) { // データのコピー preGeneration[i][j] = curGeneration[i][j]; } } for (int i = 0; i < _rank; i++) { for (int j = 0; j < _rank; j++) { int above = i == 0 ? _rank - 1 : i - 1; int below = i == _rank - 1 ? 0 : i + 1; int left = j == 0 ? _rank - 1 : j - 1; int right = j == _rank - 1 ? 0 : j + 1; int aliveCell = 0; if (preGeneration[above][left]) aliveCell++; if (preGeneration[above][j]) aliveCell++; if (preGeneration[above][right]) aliveCell++; if (preGeneration[i][left]) aliveCell++; if (preGeneration[i][right]) aliveCell++; if (preGeneration[below][left]) aliveCell++; if (preGeneration[below][j]) aliveCell++; if (preGeneration[below][right]) aliveCell++; if (!preGeneration[i][j]) { if (aliveCell == 3) { // 誕生 curGeneration[i][j] = true; } } else if (aliveCell <= 1) { // 過疎 curGeneration[i][j] = false; } else if (aliveCell <= 3) { // 生存 curGeneration[i][j] = true; } else if (aliveCell >= 4) { // 過密 curGeneration[i][j] = false; } } } for (int i = 0; i < _rank; i++) { for (int j = 0; j < _rank; j++) { // 前世と現世で状態が変われば反転させる if (preGeneration[i][j] != curGeneration[i][j]) { if (Application.Current.Dispatcher.CheckAccess()) { ToggleCellColor(GetCell(i, j)); } else { Application.Current.Dispatcher.Invoke(() => { ToggleCellColor(GetCell(i, j)); }); } } } } } private void RandomButton_Click(object sender, RoutedEventArgs e) { Trace.WriteLine(MethodBase.GetCurrentMethod().Name); try { InitializeGeneration(); // 全体の1/3のセルを生存状態にする int totalCellCount = _rank * _rank; var randomList = GetRandomRange(0, totalCellCount, totalCellCount / 3); for (int i = 0; i < _rank; i++) { for (int j = 0; j < _rank; j++) { DataGridCell cell = GetCell(i, j); SolidColorBrush brush = cell.Background as SolidColorBrush; if (randomList.Any(_r => _r / _rank == i && _r % _rank == j)) { // 黒 curGeneration[i][j] = true; if (brush != Brushes.Black) cell.Background = Brushes.Black; } else { // 白 curGeneration[i][j] = false; if (brush == Brushes.Black) cell.Background = Brushes.White; } } } } catch (Exception ex) { MessageBox.Show(ex.ToString(), "Exception Occurred !!"); } } private List<int> GetRandomRange(int min, int max, int count) { if (min > max || count <= 0) return null; Random random = new Random(DateTime.Now.Millisecond); List<int> list = new List<int>(); while (list.Count < count) { int r = random.Next(min, max); if (list.Contains(r)) continue; list.Add(r); } return list; } private DataGridCell GetCell(int i, int j) { DataGridRow row = dgSample.ItemContainerGenerator.ContainerFromIndex(i) as DataGridRow; DataGridCellsPresenter presenter = GetVisualChild<DataGridCellsPresenter>(row); return presenter.ItemContainerGenerator.ContainerFromIndex(j) as DataGridCell; } private T GetVisualChild<T> (Visual parent) where T : Visual { T child = default(T); int visCt = VisualTreeHelper.GetChildrenCount(parent); for (int i = 0; i < visCt; i++) { Visual v = VisualTreeHelper.GetChild(parent, i) as Visual; child = v as T; if (child == null) child = GetVisualChild<T>(v); if (child != null) break; } return child; } private void ToggleCellColor(DataGridCell cell) { SolidColorBrush curCellColor = cell.Background as SolidColorBrush; if (curCellColor == Brushes.Black) { cell.Background = Brushes.White; } else { cell.Background = Brushes.Black; } } } }ちなみに途中記載している行数・列数からセルを取得するメソッドについては、簡便化の為エラー処理は一切していない。
もし実際に書く場合にはもう少し丁寧に書いた方がいいです。今回はもう開き直って全部xaml.csに書きましたが、性分的にはxaml.csには1行も書きたくない派なので
次回、MVVMパターンに則って書き直しを行っていきます。
- 投稿日:2020-11-29T14:21:42+09:00
動的に変化するXMLファイルをLinQで読み込む【C#】
タイトルの件の記事は既出とは思うが、
投稿の練習&後輩の教育用にローカライズされたものが欲しかったので。プロジェクト作成(本題と関係ないので読み飛ばしてね)
新しいプロジェクトの作成でWindowsフォームアプリケーション(.NET Framework)を選択
適当に名前つけて作成
とりあえずテンプレートを用意。
ここからボタンとテキストボックスを追加してXMLを読み込む処理を入れていきます。
ボタンとテキストボックス、あとイベント追加(本題と関係ないので読み飛ばしてね その2)
コントロール(ボタンとテキストボックス)の追加
それぞれツールボックスからドラッグ&ドロップで追加できる。
テキストボックスは複数行表示させたいので、
プロパティ > プロパティ >Multiline = True
に設定
そしていい感じにテキストボックスを大きくする。
ボタンの
プロパティ > イベント > Click の所をダブルクリックして、
クリックイベントを追加する。
コードが自動生成される。
ここから本題:XML読み込みをLINQで実装する
1. XMLファイルの作成
まずは読み込むためのXMLファイルを作成する。
ひな形はこんな感じData.xml<?xml version="1.0" encoding="utf-8" ?>
別にメモ帳でも秀丸でも書ければ何でもいいけど、
visualStudioで書くなら
ソリューションエクスプローラー > 何もないところで右クリック > 追加 > 新しい項目
(またはCtrl + Shift + A でも良い)
新しい項目の追加 ウィンドウが開くので XMLファイル を選択し、
適当な名前つけて追加する
XMLファイルが自動生成される。
まずはここに読み込ませたいデータを書いていきます。
今回読み込みたいXMLのデータはこんな感じData.xml<?xml version="1.0" encoding="utf-8" ?> <Pokemon> <Gosannke> <PokeName>Fushigidane</PokeName> <PokeName>Hitokage</PokeName> <PokeName>Zenigame</PokeName> </Gosannke> <Fushigidane> <Type>kusa</Type> <Type>doku</Type> <Waza>Taiatari</Waza> <Waza>Nakigoe</Waza> <Waza>Turunomuti</Waza> </Fushigidane> <Hitokage> <Type>honoo</Type> <Waza>Hikkaku</Waza> <Waza>Nakigoe</Waza> </Hitokage> <Zenigame> <Type>mizu</Type> <Waza>Taiatari</Waza> <Waza>Shippowohuru</Waza> <Waza>Mizudeppo</Waza> <Waza>Karanikomoru</Waza> </Zenigame> </Pokemon>表にするとこう
ポケモン名 タイプ わざ フシギダネ くさ / どく たいあたり / なきごえ / つるのムチ ヒトカゲ ほのお ひっかく / なきごえ ゼニガメ みず たいあたり / しっぽをふる / みずでっぽう / からにこもる 見てわかる通り、タイプとわざの数にばらつきがあります。
(最終的にこういうデータを扱いたいという話)とりあえず手始めに、以下のデータを読み込む処理をLINQで書いてみます。
Data.xml<?xml version="1.0" encoding="utf-8" ?> <Pokemon> <Gosannke> <PokeName>Fushigidane</PokeName> <PokeName>Hitokage</PokeName> <PokeName>Zenigame</PokeName> </Gosannke> </Pokemon>2. XMLを読み込む
usingを2つ追加し、パスを取得して読み込んでみます。
Form.csusing System; using System.Windows.Forms; using System.IO; using System.Data; using System.Linq; // 追加したusing using System.Xml.Linq; // 追加したusing using System.Collections.Generic; private void Button1_Click(object sender, EventArgs e) { // カレントディレクトリを2つ上の階層に設定し直す Directory.SetCurrentDirectory(@"..\..\"); // Data.xmlのパスを取得 string xmlFilePath = Directory.GetCurrentDirectory() + @"\Data.xml"; // Data.xmlを読み込む XElement pokeData = XElement.Load(xmlFilePath); // 御三家ポケモンの名前を全て取得 IEnumerable<string> GosannkeInfo = from poke in pokeData.Elements("Gosannke").Elements("PokeName") select poke.Value; // 取得したポケモンの名前をテキストボックスに出力 foreach (string pokeName in GosannkeInfo) { textBox1.Text += pokeName + "\r\n"; } // カレントディレクトリを元の場所に戻す Directory.SetCurrentDirectory(@".\bin\Debug"); }実行してボタンを押すとこんな感じ。
ポケモンの名前を取得できました。
3. 【応用】取得したタグ名の中身を読みこむ
では本命のこのxmlを読み込んでみましょう
Data.xml<?xml version="1.0" encoding="utf-8" ?> <Pokemon> <Gosannke> <PokeName>Fushigidane</PokeName> <PokeName>Hitokage</PokeName> <PokeName>Zenigame</PokeName> </Gosannke> <Fushigidane> <Type>kusa</Type> <Type>doku</Type> <Waza>Taiatari</Waza> <Waza>Nakigoe</Waza> <Waza>Turunomuti</Waza> </Fushigidane> <Hitokage> <Type>honoo</Type> <Waza>Hikkaku</Waza> <Waza>Nakigoe</Waza> </Hitokage> <Zenigame> <Type>mizu</Type> <Waza>Taiatari</Waza> <Waza>Shippowohuru</Waza> <Waza>Mizudeppo</Waza> <Waza>Karanikomoru</Waza> </Zenigame> </Pokemon>先ほどの
foreach
の箇所を以下のように書き換えました。
foreach
がネストしてて冗長に見えますね…。Form.cs// 取得したポケモンの名前をテキストボックスに出力 foreach (string pokeName in PokeInfo) { textBox1.Text += pokeName + "\r\n"; // <Type>要素の取得 IEnumerable<string> TypeInfo = from type in pokeData.Elements(pokeName).Elements("Type") select type.Value; textBox1.Text += "TYPE : " + "\r\n"; foreach (string Type in TypeInfo) { textBox1.Text += " " + Type + "\r\n"; } // <Waza>要素の取得 IEnumerable<string> WazaInfo = from waza in pokeData.Elements(pokeName).Elements("Waza") select waza.Value; textBox1.Text += "WAZA : " + "\r\n"; foreach (string Waza in WazaInfo) { textBox1.Text += " " + Waza + "\r\n"; } // 見栄えのための改行 textBox1.Text += "\r\n"; }一応動作自体は期待通り。
でもコードが気持ち悪いのでforeach
部分をLINQで書き直してみましょう。
4. foreach文をLINQにリファクタリングする
foreach
のデメリットは、単純にネストが深く可読性が悪くなる点です。
動作を変えず、可読性をよくするために、LINQで書き直します。
分かり易いように、<Type>
要素の部分だけをLINQに書き換えました。Form.cs// 取得したポケモンの名前をテキストボックスに出力 foreach (string pokeName in PokeInfo) { textBox1.Text += pokeName + "\r\n"; // <Type>要素の取得 IEnumerable<string> TypeInfo = from type in pokeData.Elements(pokeName).Elements("Type") select type.Value; textBox1.Text += "TYPE : " + "\r\n"; TypeInfo.ToList().ForEach(Type => { textBox1.Text += " " + Type + "\r\n"; }); // ↑と↓を見比べると分かり易い! // <Waza>要素の取得 IEnumerable<string> WazaInfo = from waza in pokeData.Elements(pokeName).Elements("Waza") select waza.Value; textBox1.Text += "WAZA : " + "\r\n"; foreach (string Waza in WazaInfo) { textBox1.Text += " " + Waza + "\r\n"; } // 見栄えのための改行 textBox1.Text += "\r\n"; }
<Waza>
要素も同様にLINQで書いてみましょう。Form.cs// 取得したポケモンの名前をテキストボックスに出力 foreach (string pokeName in PokeInfo) { textBox1.Text += pokeName + "\r\n"; // <Type>要素の取得 IEnumerable<string> TypeInfo = from type in pokeData.Elements(pokeName).Elements("Type") select type.Value; textBox1.Text += "TYPE : " + "\r\n"; TypeInfo.ToList().ForEach(Type => { textBox1.Text += " " + Type + "\r\n"; }); // <Waza>要素の取得 IEnumerable<string> WazaInfo = from waza in pokeData.Elements(pokeName).Elements("Waza") select waza.Value; textBox1.Text += "WAZA : " + "\r\n"; WazaInfo.ToList().ForEach(Waza => { textBox1.Text += " " + Waza + "\r\n"; }); // 見栄えのための改行 textBox1.Text += "\r\n"; }タグ要素の取得は、共通の処理なので関数化しましょう。
Form.csprivate void OutputPokeData(XElement pokeData, string pokeName, string tag) { IEnumerable<string> Infos = from Info in pokeData.Elements(pokeName).Elements(tag) select Info.Value; textBox1.Text += tag + " : " + "\r\n"; Infos.ToList().ForEach(Info => { textBox1.Text += " " + Info + "\r\n"; }); }Form.cs// 取得したポケモンの名前をテキストボックスに出力 foreach (string pokeName in PokeInfo) { textBox1.Text += pokeName + "\r\n"; // <Type>要素の取得 OutputPokeData(pokeData, pokeName, "Type"); // <Waza>要素の取得 OutputPokeData(pokeData, pokeName, "Waza"); // 見栄えのための改行 textBox1.Text += "\r\n"; }とっても見やすくなりましたね!
最後にこのforeach
もLINQに書き直して、ソース全体を見てみましょう。Form.csusing System; using System.Collections.Generic; using System.Data; using System.Linq; using System.Windows.Forms; using System.Xml.Linq; using System.IO; namespace XMLReader { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void Button1_Click(object sender, EventArgs e) { // カレントディレクトリを2つ上の階層に設定し直す Directory.SetCurrentDirectory(@"..\..\"); // Data.xmlのパスを取得 string xmlFilePath = Directory.GetCurrentDirectory() + @"\Data.xml"; // Data.xmlを読み込む XElement pokeData = XElement.Load(xmlFilePath); // 御三家ポケモンの名前を全て取得 IEnumerable<string> PokeInfo = from poke in pokeData.Elements("Gosannke").Elements("PokeName") select poke.Value; // 取得したポケモンの名前をテキストボックスに出力 PokeInfo.ToList().ForEach(pokeName => { textBox1.Text += pokeName + "\r\n"; // ポケモン名表示 OutputPokeData(pokeData, pokeName, "Type"); // <Type>要素の取得 OutputPokeData(pokeData, pokeName, "Waza"); // <Waza>要素の取得 textBox1.Text += "\r\n"; // 見栄えのための改行 }); // カレントディレクトリを元の場所に戻す Directory.SetCurrentDirectory(@".\bin\Debug"); } private void OutputPokeData(XElement pokeData, string pokeName, string tag) { IEnumerable<string> Infos = from Info in pokeData.Elements(pokeName).Elements(tag) select Info.Value; textBox1.Text += tag + " : " + "\r\n"; Infos.ToList().ForEach(Info => { textBox1.Text += " " + Info + "\r\n"; }); } } }
- 投稿日:2020-11-29T01:08:35+09:00
Azure DevOpsのPipelineのtaskでMSTestを動かす方法
結論
PipelineのtaskでVSTestは使わず、DotNetCoreCLIのtestを使うこと
Azure DevOpsのテストの設定にはまる
Azure DevOpsのPipelineでMSTestを動かしたいと思いました。以前ユニットテストをAzure DevOpsにのせた時は特に苦労した覚えもなかったので(以前試したのは.NET FrameworkでAzure DevOpsもyamlではなくGUIで設定していました)、今回も特に苦労することは無いだろうと思っていました。ところがいざPipelineで動かそうと思ったら設定にめちゃめちゃはまりました。結局結論で解決出来たのですが、それまでに結構時間を取られたのでその行程を書いておきます。
Pipelineで動かす具体例
プロジェクトを作って、以下のようなテストを作ります。
using Microsoft.VisualStudio.TestTools.UnitTesting; namespace UnitTestProject1 { [TestClass] public class UnitTest1 { [TestMethod] public void TestMethod1() { int a = 1; Assert.AreEqual(a, 1); } [TestMethod] public void TestMethod2() { int a = 2; Assert.AreEqual(a, 1); } } }ローカルでVisual Studioのテストエクスプローラーで動かせばTestMethod2は当然失敗します。
ところがこのプロジェクトをAzure DevOpesのPipelineのTaskにVSTestを作って動かした場合、このタスクは成功します!?
- task: VSTest@2 inputs: platform: '$(buildPlatform)' configuration: '$(buildConfiguration)'VSTestの設定を変えてみる
つまり、VSTestのデフォルトの設定ではテスト結果に連動して成功、失敗を返さない状態です。なんでデフォルトがこの状態になっているのか謎です。しょうがないので設定を変えてみました。先ずSelect tests usingという項目があって、Test assembliesになっていたので、Test runを選択してみました。runなら実行するだろうと。
ところがです。実はこれテストが成功の状態でも失敗になってしまいます。つまりただの設定ミスです。
エラーを調べてみる
##[error]Input validation failed with exception: Invalid on demand test run id 0. Has to be greater than 0. ##[error]Error: The process 'D:\a\_tasks\VSTest_ef087383-ee5e-42c7-9a53-ab56c98420f9\2.170.1\Modules\DTAExecutionHost.exe'エラーをググってみるとstackoverflow、GitHubのissues、MSの公式ドキュメントなど色々情報は出ます。設定が悪い、バージョンが悪い、パスが悪い、Visual Studio Test Platform Installerを入れろなど。調べた感じ掲示板で質問だけ投げていて答えが返ってない例や、色々話したけれど結局解決していない例なども出るのでみんなはまっているけれど解決出来ていないのかな?と思います。これ以外にもVSTestの設定を色々変えて、何十回もトライ&エラーしましたが、結局テスト結果と連動してVSTestが動くように出来ませんでした。(むしろこの解決方法は誰か教えて下さい)
Azure DevOpesのtaskを変えてみる
で、MSTestをテスト結果と連動して動かす方法ですが、VSTestを使わないことで解決しました。具体的には.NET Coreのtaskでtestを選ぶ。これだけです。
- task: DotNetCoreCLI@2 inputs: command: 'test'これで、何の設定を加えなくてもちゃんと実際のテスト結果と連動して動く状態になりました。
まとめ
MSTestを動かすのであれば、どう考えてもVSTest一択だと思っていたので、設定を色々変えて試した見ましたが、結局はVSTestを使わずDotNetCoreCLIのtestを使うという結論でした。Azure DevopsのデフォルトのyamlでもVSTestが設定されているので、VSTestのデフォルトの設定のままで普通にテスト結果と連動して動くようにして欲しいです。。