20190820のC#に関する記事は12件です。

[C#/DesktopBridge] WPFをUWPパッケージしたときのUWPとしての動作確認のやりかた

■やりたいこと

WPFを、DAC(Desktop App Converter)を使ってUWP(appx)パッケージにするアプリにて、appxにしたときの動作を確認したい。

■やり方

「アプリケーションパッケージプロジェクト」を使って、UWPと同じように動作する環境を作る。

■手順

appx(UWP)パッケージしたいUWPでないアプリをつくる

今回は、.NetFramework(4.7.2)のコンソールアプリを作成することにした。

下記のアプリでやっているのは、UWPパッケージしたアプリでしか動作しないUWPのAPIが動くかどうかで、UWPパッケージとして動いているかどうかを確認するということ。

Program.cs
using System;
using System.Diagnostics;

namespace ConsoleApp12
{
    class Program
    {
        static void Main(string[] args)
        {
            string path = string.Empty;
            try
            {
                // UWPアプリ
                var localDir = Windows.Storage.ApplicationData.Current.LocalFolder;
                path = localDir.Path;
            }
            catch (Exception ex)
            {
                // UWPアプリでない
                Debug.WriteLine(ex.Message);
                path = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
            }
            Debug.WriteLine("target paht is : " + path);
        }
    }
}

アプリケーションパッケージプロジェクトを追加する

ソリューションに、下図のアプリケーションパッケージプロジェクトを追加する。

image.png

アプリケーションパッケージプロジェクトに対象のアプリを追加する

アプリケーションパッケージプロジェクトの中の「アプリケーション」を右クリックして「参照の追加」をク選択し、パッケージしたいプロジェクト(ここではConsoleApp12)を追加する。
image.png

ビルドして、アプリケーションパッケージプロジェクトをスタートアップPJに設定する

ソリューション全体をビルドした後、アプリケーションパッケージプロジェクトを右クリックして、「スタートアッププロジェクトに設定」をクリックし、F5を押したらこちらのPJから起動するようにする。

F5(デバッグ実行)を押す

アプリケーションパッケージプロジェクトを起動する。
これで、デバッグできる。Breakを貼れば、そこでとめることもできる。

■実行結果

WPFの場合

コンソールアプリをスタートアップにして起動した場合は、Windows.Storage.ApplicationData.Current.LocalFolderで例外発生して、catchのほうを通る。

実行結果.txt
target path is : C:\Users\masa\AppData\Local

UWP(パッケージPJから起動した)場合

パッケージPJをスタートアップにして起動した場合は、Windows.Storage.ApplicationData.Current.LocalFolderで例外発生せず、パスを取得できる。

実行結果.txt
target path is : C:\Users\masa\AppData\Local\Packages\6c840429-c91b-4fbf-9592-af26324a4a1e_erpevgh2z069r\LocalState



■気になったところ

ここまでで、一旦デバッグはできるようになった。
以下は、調べる中で気になったことや、注意すべき点をメモしておく。
(調べながらのメモなので、まとまりありません)

出力フォルダはどこか?

アプリケーションパッケージプロジェクトの設定としては、デフォで下記のようになっている。
image.png

ので、ビルドすると、下記のようなフォルダにexe一式ができた。
<ソリューションフォルダ>\<プロジェクトフォルダ>\bin\<プラットフォーム>\<コンフィグレーション>\<アプリ名フォルダ> の中。
image.png

出力フォルダは上記だが、F5を押して行う「デバッグ」で動く際は、別のフォルダに、パッケージに含まれるもの一式が出来上がって動くっぽい。
<ソリューションフォルダ>\<プロジェクトフォルダ>\bin\<プラットフォーム>\<コンフィグレーション>\AppX\ の中。
image.png
image.png

この中に、アプリのexeと、exeが参照してるdllなどが、一式入る。

(参考)パッケージの作成

いろいろ調べてると、DACはすでにマイクロソフトの推奨から外れているらしい。

現状にわか状態なので詳しくはあとで調べるとして、とりあえず下記のようにすると、パッケージを作れる様子。

- パッケージPJを右クリックして [ストア] > [アプリ パッケージの作成]を選択

ここでパッケージを作成すると、下記のようなものができる。

<プロジェクトフォルダ>\AppPackages フォルダ

その中の<PJ名>_<バージョン>_<コンフィグ>_Testフォルダに、.appxbundleまたは.msixbundleを含むインストーラー一式が出来上がる。(.cerの証明書ファイルも一緒にできてる)

<プロジェクトフォルダ>\bin\<プラットフォーム>\<コンフィグレーション> フォルダ

ここに、.appxまたは.msixができてる。

(参考)パッケージ作成時、appxとmsixのどちらができるのか?

.appxbundleができるときと、.msixbundleができるときがある。どっちができるか決める条件はなんなのか?

19/08/21 追記
⇒VS2019 v16.1.6で試してみたところ、下記っぽい。
image.png

パッケージプロジェクトの「最小バージョン」1803(10.0 ビルド17134)以前にすると、appxとappxbundleが出来上がり、1809(10.0 ビルド17763)以降にすると、msixとmsixbundleが出来上がる。
image.png

なんとなく試してみて上記だと分かったが、どこかに説明かいているのか???
MSdocs色々見たつもりだが、appxとmsixのできる条件、見つけられなかった...

19/08/21 追記その2
下記が、そういうことか??
image.png
MSDocsより引用。

(参考)注意・ハマりかけた事

アプリが参照してるexeの配置についての注意

アプリが使うdllは、パッケージPJが、参照の設定をもとに自動でアプリのexeと同じ階層に入れてくれる。試したところ、

  1. アプリの参照設定に入っていて「ローカルにコピー」がtrueになっているもの
  2. アプリが参照しているdllが、さらに参照しているdll

は、自動で入れてくれるっぽい。

但し、上の2.は、「アプリが参照しているdll」と同じフォルダに「さらに参照しているdll」が入っていないと自動で配置してくれないっぽい。
なので、dll類を、ビルド後イベントで、別のところからコピーしてきているような場合については、パッケージプロジェクトでは自動で配置してくれなかった。
(そういう時は、パッケージPJの方でもビルド後イベントでコピーなどしないといけないっぽい。未検証)

UWPのAPIを使うときの注意

やりたいことの本筋から少々れるが、この実験をやるうえで、パスを取得するためのUWPのAPI(Windows.Storage.ApplicationData.Current.LocalFolder)を使いたかったので、NugetでMicrosoft.Windows.SDK.Contractsを追加して使えるようにしようとしたが、下記のようなエラーが出た。

image.png

エラーの主語(アプリケーションは、...)が何を指しているのかあいまいで混乱したが、どうやらMicrosoft.Windows.SDK.Contractsが、Windowsの10.0.18362.0、つまり1903を必要とするので、それ(Contracts)を使っているConsoleApp12(今回作った実験用コンソールアプリ)のMinVerを下げろ、と言っている様子。
Microsoft.Windows.SDK.ContractsはNugetしてきていて、MinVerを下げることはできないので困った。

解決法としては、Microsoft.Windows.SDK.Contractsを使うのをやめて、以前の方法(こちら参照。.winmdファイルを参照するやり方)でUWPのAPIを呼ぶようにして対応した。

参考

WPF などの .NET Framework のアプリから UWP の API を呼ぶ
https://blog.okazuki.jp/entry/2018/03/29/101601

.NET のプロジェクトから WinRT API を呼ぶのが凄く簡単になってます
https://blog.okazuki.jp/entry/2019/05/09/115020

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【C#】SQLで読み込んだデータをクラスプロパティに格納して返すメソッド

SQL Serverのデータベースに接続して、読み込んだ値を変数に格納する文法を記述します。

はじめに、データベースに接続するまでの文。

Service1.svc.cs
public SavedData SetData(int selected_id)  //メソッド名 引数には主要キー、返り値にはクラス名
{
    const string connStr = @"Data Source=(local);
                        Initial Catalog=Rac-stafflist; Integrated Security=True;";

    using (var conn = new SqlConnection(connStr))
    {
        conn.Open();
        var cmd = conn.CreateCommand(); 
        // コネクションからコマンドへ-SqlConnectionに関連付けられているSqlCommandオブジェクトを作成

        cmd.Parameters.Add(new SqlParameter("@selected_id", selected_id));
        cmd.CommandText = @"SELECT Id,Name,DepartmentId,Sex,JoinedYear,Note
                            FROM dbo.Staff Where Id = @selected_id ";

ここまででデータベースに接続。以下、データベースから読み込んだ値をクラスのプロパティに代入

Service1.svc.cs
        using (var reader = cmd.ExecuteReader())
        {
            while (reader.Read())
            {
                int demoid = (int)reader["Id"]; 
                //クラスプロパティ'ID'のデータ型がintの場合は、このように変換

                setdata.ID = demoid.ToString();
                setdata.NAME = (string)reader["Name"];
                setdata.DEPARTMENTID = (int)reader["DepartmentId"];
                setdata.SEX = (int)reader["Sex"];
                setdata.JOINEDYEAR = (int)reader["JoinedYear"];
                setdata.NOTE = (string)reader["Note"];
            }
        }
    }

    //メソッドが呼び出されたインスタンス自身を返す
    return setdata;
}

これで、この返り値にあてたクラスをインスタンス化して、変数を自由に格納できる。

追記

編集リクエストをくださった@nogic1008さん、ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Enum制約下のジェネリック・クラスでenumとintの相互キャスト (C#)

前提

  • C# 7.3

やりたいこと

  • ジェネリックなenumを扱うクラスclass <TEnum> where TEnum : System.Enumで、TEnumintを相互に変換したい。

できなかったこと

普通にキャストしてみたところ、コンパイラが「このキャストはできない」とエラーを吐きました。

public class EnumeratedArray<TEnum, TObj> where TEnum : Enum {
    private TObj [] objects;
    public TObj this [TEnum n] { get { return objects [(int) n]; } } // エラー
    public TEnum this [TObj o] { get { return (TEnum) Array.IndexOf (objects, o); } } // エラー
}
// この例は、やりたいことを端的に表現したもので、やりたいことそのものではありません。

通常であれば、intとenumの相互キャストは可能なはずで、ジェネリックだからできないのでしょうね。

できたこと

System.Objectとの変換メソッドを使いました。

public class EnumeratedArray<TEnum, TObj> where TEnum : Enum {
    private TObj [] objects;
    public TObj this [TEnum n] { get { return objects [Convert.ToInt32 (n)]; } }
    public TEnum this [TObj o] { get { return (TEnum) Enum.ToObject (typeof (TEnum), Array.IndexOf (objects, o)); } }
}

とはいえ、この方法なら、型制約がなくても(7.3未満でも)書けてしまうんですね。

わかったこと

  • C# 7.3でEnum制約が書けるようになったけど、存外と制限が厳しい。

蛇足

EnumeratedList.cs
using System;
using System.Collections.Generic;

/// <summary>EnumでインデックスするList</summary>
/// <typeparam name="TEnum">インデックスの型</typeparam>
/// <typeparam name="TObj">要素の型</typeparam>
public class EnumeratedList<TEnum, TObj> : List<TObj> where TEnum : Enum {

    /// <summary>インデックスが範囲内か調べる</summary>
    public bool ContainsKey (TEnum n) {
        var index = Convert.ToInt32 (n);
        return (index >= 0 && index < this.Count);
    }

    /// <summary>正引き</summary>
    public TObj this [TEnum n] => this [Convert.ToInt32 (n)];

    /// <summary>逆引き</summary>
    public TEnum EnumOf (TObj o) => (TEnum) Enum.ToObject (typeof (TEnum), this.IndexOf (o));

}

素直にDictionary<TEnum, TObj>を使っておけと言われそうですね。

参考

以下を参考にさせていただきました。

いつもありがとうございます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

サイボウズGaroonの社内メールをC#で一括送信する

はじめに

サイボウズ社のGaroon(ガルーン)は、組織内の情報共有を目的としたグループウェアのひとつだ。主に中堅~大企業をターゲットにしている。
電子メールの機能はあるもののSMTPサーバを持たないため、外部プログラムからメールを送信するにはGaroonのWeb APIを叩く必要がある。

今回、Web APIを利用して、社内メールを一括送信するプログラムをC#で作成したので開発手順を紹介する。

謝辞

開発にあたり、下記の記事を大いに参考にさせていただいた。
深く謝意を表すとともに、本記事とあわせて参照して欲しい。

SOAPとREST

Web APIを構成する2大要素、SOAPとRESTの違いについて簡単に説明しておく。

SOAPはWebサービスの初期に定義された古くからあるプロトコル(規格)で、HTTPのPOSTメソッドでXMLデータをやり取りする。WSDLと呼ばれるインターフェース仕様(XMLで記述されている)からネイティブAPIのコードを自動生成して使うことが多い。ステートフルである。JavaC#と相性が良く、WebサービスといえばSOAPといわれるくらい昔は一般的だった。

RESTはプロトコルというより概念とか設計思想に近い。HTTPのGET/POST/PUT/DELETEメソッドで主にjsonデータをやり取りする。フォーマットが厳密に決まっているわけでは無い。ステートレスである。単純な実装なので容易に始められる。

APIといえば、筆者もかつてはRPCとかCORBAを叩きまくっていた世代だが、今日、たんにAPIといえばRESTを指すことが多い。

開発環境

  • Visual Studio 2019(C#)
  • サイボウズ Garoon(パッケージ版)

開発手順

REST APIは基本的にクラウド版でしか提供されないため、本記事ではSOAP APIで作成していく。

プロキシクラスの生成

WebサービスにアクセスするプロキシクラスReference.csを、WSDLから生成する。
オブジェクトをXMLデータにシリアライズしたり、逆にデシリアライズするのは、このプロキシクラスがやってくれる。

サービス参照の追加

赤枠内を順にクリックする。
image.png
image.png
image.png

Web参照の追加

URLの欄にWSDLの場所を入力する。
WSDLの場所は、公式ドキュメントGaroon SOAP APIの共通仕様に記載されている。
wsdl.png

プロキシクラスの修正

ソリューションエクスプローラ上部にある [すべてのファイルを表示] ボタンをクリックし、自動生成されたReference.csを表示する。
image.png
Reference.csにGaroonのSOAPヘッダを追加する。手順は【VisualStudio2017でガルーンAPIを使ってメッセージを送ってみる】が詳しいので、ここでは割愛する。

リクエストクラスの作成

プロキシクラスの下記APIを呼ぶリクエストクラスを作成する。

Garoon共通のSOAPヘッダをメンバ変数に定義し、コンストラクタで生成するようにした。認証情報もコンストラクタで受け取る。

GaroonRequest.cs
using System;
using System.IO;
using System.Linq;
using MindWood.GaroonClientApp.GaroonService;

namespace MindWood.GaroonClientApp
{
    /*
     * Garoonリクエストクラス
     */
    class GaroonRequest
    {
        // Garoon共通SOAPヘッダ
        private ActionElement actionElement;
        private UsernameTokenElement userNameTokenElement;
        private SecurityElement securityElement;
        private TimestampElement timeStampElement;

        // コンストラクタ
        public GaroonRequest(string username, string password)
        {
            actionElement = new ActionElement();
            userNameTokenElement = new UsernameTokenElement();
            securityElement = new SecurityElement();
            timeStampElement = new TimestampElement();

            userNameTokenElement.Username = username;
            userNameTokenElement.Password = password;
            securityElement.usernameToken = userNameTokenElement;

            timeStampElement.Created = DateTime.UtcNow;
            timeStampElement.Expires = timeStampElement.Created.AddDays(8);
        }

        // ログイン名からユーザIDを取得する
        public UserInfo BaseGetGetUsersByLoginName(string login_name)
        {
            actionElement.actionValue = "BaseGetUsersByLoginName";
            BaseBinding api = new BaseBinding {
                action = actionElement,
                security = securityElement,
                timeStamp = timeStampElement
            };
            string[] param = { login_name };

            UserInfo user = new UserInfo();
            try {
                UserType[] resp = api.BaseGetUsersByLoginName(param);
                user.id = resp[0].key;
                user.name = resp[0].name;
                user.email = resp[0].email;
            } catch (Exception ex) {
                Console.WriteLine(ex.Message);
            }
            return user;
        }

        // メッセージを送信する
        public string MessageCreateThreads(string user_id, string subject, string body, string fullpath_file)
        {
            actionElement.actionValue = "MessageCreateThreads";
            MessageBinding api = new MessageBinding {
                action = actionElement,
                security = securityElement,
                timeStamp = timeStampElement
            };

            string id_str = null;
            try {
                ThreadType threadType = new ThreadType();
                ThreadTypeAddressee threadTypeAddressee = new ThreadTypeAddressee();
                content content = new content();
                ThreadTypeFolder threadTypeFolder = new ThreadTypeFolder();
                ThreadTypeFollow threadTypeFollow = new ThreadTypeFollow();
                MessageCreateThreadType messageThreadType = new MessageCreateThreadType();
                MessageCreateThreadsRequestType messageThreadsRequestType = new MessageCreateThreadsRequestType();

                threadType.id = "dummy";  // ID
                threadType.version = "dummy";  // スレッドのバージョン
                threadTypeFolder.id = "dummy";  // フォルダID
                threadType.folder = new ThreadTypeFolder[1];
                threadType.folder[0] = threadTypeFolder;
                threadTypeFollow.id = "dummy";
                threadType.follow = new ThreadTypeFollow[1];
                threadType.follow[0] = threadTypeFollow;
                threadType.confirm = false;  // 閲覧状況の確認は不要

                // タイトル
                threadType.subject = subject;

                // 本文
                content.body = body;
                threadType.content = content;

                int i;

                // 宛先
                i = 0;
                string[] ids = user_id.Split(',');
                threadType.addressee = new ThreadTypeAddressee[ids.Length];
                foreach (var id in ids) {
                    threadTypeAddressee = new ThreadTypeAddressee();
                    threadTypeAddressee.user_id = id;
                    threadTypeAddressee.name = "dummy";
                    threadType.addressee[i] = threadTypeAddressee;
                    i++;
                }

                // 添付ファイル
                if (fullpath_file != null) {
                    i = 0;
                    string[] files = fullpath_file.Split(',');
                    MessageCreateThreadTypeFile[] typeFiles = new MessageCreateThreadTypeFile[0];
                    contentFile[] contFiles = new contentFile[0];

                    foreach (var file in files) {
                        // 添付ファイルをバイト配列に読み込む
                        FileStream fs = new FileStream(file, FileMode.Open, FileAccess.Read);
                        byte[] bs = new byte[fs.Length];
                        fs.Read(bs, 0, bs.Length);
                        fs.Close();

                        // ファイル実体
                        MessageCreateThreadTypeFile typeFile = new MessageCreateThreadTypeFile();
                        typeFile.content = bs;
                        typeFile.id = i.ToString();

                        Array.Resize(ref typeFiles, i + 1);
                        typeFiles[i] = typeFile;

                        // ファイル情報
                        contentFile contFile = new contentFile();
                        contFile.id = i.ToString();
                        contFile.size = (ulong)bs.Length;
                        contFile.name = Path.GetFileName(file);
                        contFile.mime_type = System.Web.MimeMapping.GetMimeMapping(contFile.name);

                        Array.Resize(ref contFiles, i + 1);
                        contFiles[i] = contFile;

                        i++;
                    }
                    messageThreadType.file = typeFiles;
                    threadType.content.file = contFiles;
                }

                // メッセージの送信
                messageThreadType.thread = threadType;
                messageThreadsRequestType.create_thread = new MessageCreateThreadType[1];
                messageThreadsRequestType.create_thread[0] = messageThreadType;

                ThreadType[] resp = api.MessageCreateThreads(messageThreadsRequestType);

                if (resp.Any()) {
                    id_str = resp[0].id.ToString();
                }

            } catch (Exception ex) {
                Console.WriteLine(ex.Message);
            }
            return id_str;
        }

    }
}

ユーザ情報を管理するクラスは次の通り。これなら構造体で良いかもしれない。

UserInfo.cs
namespace MindWood.GaroonClientApp
{
    class UserInfo
    {
        public string id;
        public string name;
        public string email;
    }
}

リクエストクラスの使い方

インスタンスの作成方法

GaroonRequest req = new GaroonRequest(自ログイン名, パスワード);

メールの送信方法

宛先ログイン名(連絡帳から確認できる)から、システム内のユーザIDを取得し、メールを送信する。
添付ファイルが要らなければ、nullを添付ファイル名に渡せば良い。

UserInfo user = req.BaseGetGetUsersByLoginName(宛先ログイン名);
if (user == null) {
    // エラー時の処理
} else {
    string res = req.MessageCreateThreads(user.id, タイトル, 本文, 添付ファイル名);
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

サイボウズGaroonの社内メールをC#から送信する

はじめに

サイボウズ社のGaroon(ガルーン)は、組織内の情報共有を目的としたグループウェアのひとつだ。主に中堅~大企業をターゲットにしている。
電子メールの機能はあるもののSMTPサーバを持たないため、外部プログラムからメールを送信するにはGaroonのWeb APIを叩く必要がある。

今回、Web APIを利用して、社内メールを一括送信するプログラムをC#で作成したので開発手順を紹介する。

謝辞

開発にあたり、下記の記事を大いに参考にさせていただいた。
深く謝意を表すとともに、本記事とあわせて参照して欲しい。

SOAPとREST

Web APIを構成する2大要素、SOAPとRESTの違いについて簡単に説明しておく。

SOAPは、Webサービスの初期に定義された古くからあるプロトコル(規格)で、HTTPのPOSTメソッドでXMLデータをやり取りする。WSDLと呼ばれるインターフェース仕様(XMLで記述されている)からネイティブAPIのコードを自動生成して使うことが多い。ステートフルである。JavaC#と相性が良く、WebサービスといえばSOAPといわれるくらい昔は主流だった。

RESTは、プロトコルというより概念とか設計思想に近い。HTTPのGET/POST/PUT/DELETEメソッドで主にjsonデータをやり取りする。フォーマットが厳密に決まっているわけでは無い。ステートレスである。単純な実装なので容易に始められる。

APIといえば、筆者もかつてはRPCとかCORBAを叩きまくっていた世代だが、今日、たんにAPIといえばRESTを指すことが多い。

開発環境

  • Visual Studio 2019(C#)
  • サイボウズ Garoon(パッケージ版)

開発手順

GaroonのREST APIは基本的にクラウド版でしか提供されないため、本記事ではSOAP APIで作成していく。

プロキシクラスの生成

WebサービスにアクセスするプロキシクラスReference.csを、WSDLから生成する。
オブジェクトをXMLデータにシリアライズしたり、逆にデシリアライズするのは、このプロキシクラスがやってくれる。

サービス参照の追加

赤枠内を順にクリックする。
image.png
image.png
image.png

Web参照の追加

URLの欄にWSDLの場所を入力する。
WSDLの場所は、公式ドキュメントGaroon SOAP APIの共通仕様に記載されている。
wsdl.png

プロキシクラスの修正

ソリューションエクスプローラ上部にある [すべてのファイルを表示] ボタンをクリックし、自動生成されたReference.csを表示する。
image.png
Reference.csにGaroonのSOAPヘッダを追加する。手順は【VisualStudio2017でガルーンAPIを使ってメッセージを送ってみる】が詳しいので、ここでは割愛する。

リクエストクラスの作成

プロキシクラスの下記APIを呼ぶリクエストクラスを作成する。

Garoon共通のSOAPヘッダをメンバ変数に定義し、コンストラクタで設定するようにした。認証情報もコンストラクタで受け取る。

GaroonRequest.cs
using System;
using System.IO;
using System.Linq;
using MindWood.GaroonClientApp.GaroonService;

namespace MindWood.GaroonClientApp
{
    /*
     * Garoonリクエストクラス
     */
    class GaroonRequest
    {
        // Garoon共通SOAPヘッダ
        private ActionElement actionElement;
        private UsernameTokenElement userNameTokenElement;
        private SecurityElement securityElement;
        private TimestampElement timeStampElement;

        // コンストラクタ
        public GaroonRequest(string username, string password)
        {
            actionElement = new ActionElement();
            userNameTokenElement = new UsernameTokenElement();
            securityElement = new SecurityElement();
            timeStampElement = new TimestampElement();

            userNameTokenElement.Username = username;
            userNameTokenElement.Password = password;
            securityElement.usernameToken = userNameTokenElement;

            timeStampElement.Created = DateTime.UtcNow;
            timeStampElement.Expires = timeStampElement.Created.AddDays(8);
        }

        // ログイン名からユーザIDを取得する
        public UserInfo BaseGetGetUsersByLoginName(string login_name)
        {
            actionElement.actionValue = "BaseGetUsersByLoginName";
            BaseBinding api = new BaseBinding {
                action = actionElement,
                security = securityElement,
                timeStamp = timeStampElement
            };
            string[] param = { login_name };

            UserInfo user = new UserInfo();
            try {
                UserType[] resp = api.BaseGetUsersByLoginName(param);
                user.id = resp[0].key;
                user.name = resp[0].name;
                user.email = resp[0].email;
            } catch (Exception ex) {
                Console.WriteLine(ex.Message);
            }
            return user;
        }

        // メッセージを送信する
        public string MessageCreateThreads(string user_id, string subject, string body, string fullpath_file)
        {
            actionElement.actionValue = "MessageCreateThreads";
            MessageBinding api = new MessageBinding {
                action = actionElement,
                security = securityElement,
                timeStamp = timeStampElement
            };

            string id_str = null;
            try {
                ThreadType threadType = new ThreadType();
                ThreadTypeAddressee threadTypeAddressee = new ThreadTypeAddressee();
                content content = new content();
                ThreadTypeFolder threadTypeFolder = new ThreadTypeFolder();
                ThreadTypeFollow threadTypeFollow = new ThreadTypeFollow();
                MessageCreateThreadType messageThreadType = new MessageCreateThreadType();
                MessageCreateThreadsRequestType messageThreadsRequestType = new MessageCreateThreadsRequestType();

                threadType.id = "dummy";  // ID
                threadType.version = "dummy";  // スレッドのバージョン
                threadTypeFolder.id = "dummy";  // フォルダID
                threadType.folder = new ThreadTypeFolder[1];
                threadType.folder[0] = threadTypeFolder;
                threadTypeFollow.id = "dummy";
                threadType.follow = new ThreadTypeFollow[1];
                threadType.follow[0] = threadTypeFollow;
                threadType.confirm = false;  // 閲覧状況の確認は不要

                // タイトル
                threadType.subject = subject;

                // 本文
                content.body = body;
                threadType.content = content;

                int i;

                // 宛先
                i = 0;
                string[] ids = user_id.Split(',');
                threadType.addressee = new ThreadTypeAddressee[ids.Length];
                foreach (var id in ids) {
                    threadTypeAddressee = new ThreadTypeAddressee();
                    threadTypeAddressee.user_id = id;
                    threadTypeAddressee.name = "dummy";
                    threadType.addressee[i] = threadTypeAddressee;
                    i++;
                }

                // 添付ファイル
                if (fullpath_file != null) {
                    i = 0;
                    string[] files = fullpath_file.Split(',');
                    MessageCreateThreadTypeFile[] typeFiles = new MessageCreateThreadTypeFile[0];
                    contentFile[] contFiles = new contentFile[0];

                    foreach (var file in files) {
                        // 添付ファイルをバイト配列に読み込む
                        FileStream fs = new FileStream(file, FileMode.Open, FileAccess.Read);
                        byte[] bs = new byte[fs.Length];
                        fs.Read(bs, 0, bs.Length);
                        fs.Close();

                        // ファイル実体
                        MessageCreateThreadTypeFile typeFile = new MessageCreateThreadTypeFile();
                        typeFile.content = bs;
                        typeFile.id = i.ToString();

                        Array.Resize(ref typeFiles, i + 1);
                        typeFiles[i] = typeFile;

                        // ファイル情報
                        contentFile contFile = new contentFile();
                        contFile.id = i.ToString();
                        contFile.size = (ulong)bs.Length;
                        contFile.name = Path.GetFileName(file);
                        contFile.mime_type = System.Web.MimeMapping.GetMimeMapping(contFile.name);

                        Array.Resize(ref contFiles, i + 1);
                        contFiles[i] = contFile;

                        i++;
                    }
                    messageThreadType.file = typeFiles;
                    threadType.content.file = contFiles;
                }

                // メッセージの送信
                messageThreadType.thread = threadType;
                messageThreadsRequestType.create_thread = new MessageCreateThreadType[1];
                messageThreadsRequestType.create_thread[0] = messageThreadType;

                ThreadType[] resp = api.MessageCreateThreads(messageThreadsRequestType);

                if (resp.Any()) {
                    id_str = resp[0].id.ToString();
                }

            } catch (Exception ex) {
                Console.WriteLine(ex.Message);
            }
            return id_str;
        }

    }
}

ユーザ情報を管理するクラスは次の通り。これなら構造体で良いかもしれない。

UserInfo.cs
namespace MindWood.GaroonClientApp
{
    class UserInfo
    {
        public string id;
        public string name;
        public string email;
    }
}

リクエストクラスの使い方

インスタンスの作成方法

GaroonRequest req = new GaroonRequest(自ログイン名, パスワード);

メールの送信方法

宛先ログイン名(連絡帳から確認できる)から、システム内のユーザIDを取得し、メールを送信する。
添付ファイルが要らなければ、nullを添付ファイル名に渡せば良い。

UserInfo user = req.BaseGetGetUsersByLoginName(宛先ログイン名);
if (user == null) {
    // エラー時の処理
} else {
    string res = req.MessageCreateThreads(user.id, タイトル, 本文, 添付ファイル名);
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

.Net(C#)を用いてZipアーカイブから解凍無しにOpenCV:Matに変換する方法(短報)

abstract

Deep LearningによるCVが多用されるようになり、これにかかわる技術開発をされている方が多いでしょう。
出力結果の画像を管理・表示する際、PythonのプログラムでGUI(それこそPyOpenCVなど)を表示することもできるでしょう。
ただ、windowsでプログラムをされている方は何となく.Netで作りたくならないでしょうか(?)。近年は.Net coreによってLinux環境下でも同じソースで動かすことができるようになっております。

method

さて、学習させて何らかの予測・画像生成を行ったとき出力を何らかの圧縮形式で一つのファイルにすることがあるでしょう。これを.Net Form上に表示しようとしたとき.Net の画像形式ImageもしくはBitmapが用いられる。これらのコンストラクタは開いたファイルオブジェクト引数にとれる。

using (var entry = ZipFile.GetEntry(filename).Open())//filenameはzipファイル内の画像ファイルを指す
{
    var bit = Bitmap(entry);
}

しかし、以下のコードは動かない。

var Mat = Mat(entry); //開いたファイルは引数にとれない

ただ、表示するにもただ結果を出力するだけでなく、少し画像の加工が必要な場合OpenCVで加工したくなる。ただ、zipの解凍はしたくない。
そこでBitmap -> Mat変換が可能であれば、それができる。
少し調べたところ

using OpenCvSharp.Extensions;//ここが重要。ToMatメソッドが追加される。

using (var entry = ZipFile.GetEntry(filename).Open())//filenameはzipファイル内の画像ファイルを指す
{
    var mat = Bitmap(entry).ToMat();
}

以上、zipの解凍を行わず,Matを生成して画像処理できるようになった。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FlyCapture2のFlyCapture2_v140.dllを単体テストから呼ぶとクラッシュ

問題

FlyCapture2のFlyCapture2Ⅾd_v140.dllを単体テストから呼ぶとクラッシュしてしまう。

現在WPF+C#デスクトップアプリからFlyCaptureSDKを用いてカメラから画像取得しております。

サンプルに従いアプリでは参照にFlyCapture2GUI_Managedd_v140.dllを追加し、
実行ファイルのパスにFlyCapture2d_v140.dllをコピーするようにしました。

この状態でこれらを使うクラスを単体テストから呼び出すとFlyCapture2d_v140.dllを読み込んだ時点でtesthost.exeが黙って落ちました。

対処法

参照dllをFlyCapture2GUI_Managed_v140.dll
コピーするdllをFlyCapture2_v140.dll
に変更します。
(分かりにくいですが_v140の前のdが無い方です。)

経緯

問題の起きた時、Windowsのイベントビューワに残るログは以下のものでした。

障害が発生しているアプリケーション名: testhost.exe、バージョン: 15.0.0.0、タイム スタンプ: 0x92c7181e
障害が発生しているモジュール名: FlyCapture2d_v140.dll、バージョン: 2.13.3.61、タイム スタンプ: 0x5ca574f7
例外コード: 0x80000003
障害オフセット: 0x0000000001178210
障害が発生しているプロセス ID: 0x98
障害が発生しているアプリケーションの開始時刻: 0x01d556344949e967
障害が発生しているアプリケーション パス: C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\Common7\IDE\Extensions\TestPlatform\testhost.exe
障害が発生しているモジュール パス: C:\Program Files\Point Grey Research\FlyCapture2\bin64\vs2015\FlyCapture2d_v140.dll
レポート ID: b7cc00c9-8fd6-49e4-9aa3-4aa73ed9bb79
障害が発生しているパッケージの完全な名前:
障害が発生しているパッケージに関連するアプリケーション ID:・・・・

この例外コード: 0x80000003で調べてみるとこんなページが引っかかりました。
ここによればエラーコードの意味はSTATUS_BREAKPOINTだそうです。

サンプルコードのビルドイベントコマンドを眺めていると、どうやらDebungとReleaseでdllを呼び変えてました。

あ、Debug用じゃない方にすればBREAKPOINT引っかからないんじゃね?と思い呼び出すdllを変更しました。

変更前
IF $(Platform)==x86 (
copy "$(ProjectDir)..\..\bin\vs2015\libiomp5md.dll" "$(TargetDir)"
IF $(ConfigurationName)==Debug (
copy "$(TargetDir)$(TargetFileName)" "$(ProjectDir)..\..\bin\vs2015\$(TargetName)d_v140$(TargetExt)"
copy "$(ProjectDir)..\..\bin\vs2015\FlyCapture2d_v140.dll" "$(TargetDir)"
) ELSE (
copy "$(TargetDir)$(TargetFileName)" "$(ProjectDir)..\..\bin\vs2015\$(TargetName)_v140$(TargetExt)"
copy "$(ProjectDir)..\..\bin\vs2015\FlyCapture2_v140.dll" "$(TargetDir)"
)
) ELSE IF $(Platform)==x64 (
copy "$(ProjectDir)..\..\bin64\vs2015\libiomp5md.dll" "$(TargetDir)"
IF $(ConfigurationName)==Debug (copy "$(TargetDir)$(TargetFileName)" "$(ProjectDir)..\..\bin64\vs2015\$(TargetName)d_v140$(TargetExt)"
copy "$(ProjectDir)..\..\bin64\vs2015\FlyCapture2d_v140.dll" "$(TargetDir)"
) ELSE (
copy "$(TargetDir)$(TargetFileName)" "$(ProjectDir)..\..\bin64\vs2015\$(TargetName)_v140$(TargetExt)"
copy "$(ProjectDir)..\..\bin64\vs2015\FlyCapture2_v140.dll" "$(TargetDir)"
)
)

変更後
IF $(Platform)==x86 (
copy "$(ProjectDir)..\..\bin\vs2015\libiomp5md.dll" "$(TargetDir)"

copy "$(TargetDir)$(TargetFileName)" "$(ProjectDir)..\..\bin\vs2015\$(TargetName)_v140$(TargetExt)"
copy "$(ProjectDir)..\..\bin\vs2015\FlyCapture2_v140.dll" "$(TargetDir)"
) ELSE IF $(Platform)==x64 (
copy "$(ProjectDir)..\..\bin64\vs2015\libiomp5md.dll" "$(TargetDir)"

copy "$(TargetDir)$(TargetFileName)" "$(ProjectDir)..\..\bin64\vs2015\$(TargetName)_v140$(TargetExt)"
copy "$(ProjectDir)..\..\bin64\vs2015\FlyCapture2_v140.dll" "$(TargetDir)"
)

DebugだろうがReleaseだろうがRelease用dllを呼び出すように変更しました。
これによって何かDebug動作に支障が出るのかは不明ですが、今のところは問題なく使えております。

単体テストのスレッド

MsTestで発生して色々調べていたら、TestではSTAとは違う動作になるとの事で、これは後付けカメラを使うとき中々に引っかかりそうだ!と思いました。

で、さらにXunit.StaFactでSTAっぽく動かせるという情報を見つけまして、試してみましたがこれもだめでした。

結果的に寄り道でしたが、単体テストがSTAでない事と、その動作が少し掴めました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityでテストコードを書いて会得した最強のTips(苦肉の策ともいう)

最強のTips(苦肉の策)が生まれるきっかけ

自分が現在開発に携わっているゲームでは、作中に多くのシナリオが含まれています。
その数はデータにして200を超えていますが、このデータが増えるにつれて、ある問題が発生しました。

それは、シナリオデータの数が多すぎて、不具合のあるデータがあることに気付けないということです。
(特定の選択肢を選ぶと、進行不能になるデータがいくつか見られました)

これに対処するため、「全シナリオの動作チェックを行うUIのテストコードを書こう!」と思ったのが、事の始まりです。

まさか、あんな苦肉の策を取ることになるとは、この時は考えてもいませんでした…

目次(Tips一覧)

自分がテストコードを書いて会得したTipsを紹介します。

ちなみに、最強のTips(苦肉の策)は「6. テストの失敗が後続のテストにまで影響を与えないようにする」です。
それ以外は、まとも(のはず…)なTipsになります。

1. Play Modeテストで使うAttributeとその注意点
2. テスト時間を短縮する
3. 手軽にUIテストをできるようにする
4. テストの失敗をハンドリングする
5. コード側からTest Runnerを制御する
6. テストの失敗が後続のテストにまで影響を与えないようにする
7. CommandLineからTest Runnerを実行する

Tipsを紹介する前に

テストコードを書くにあたって、Unity上でテストできるというメリットから、Unityに標準で付属しているTest RunnerのPlay Modeテストを使用しています。

Play Modeテストはコルーチンを使って記述され、このコルーチンが最後までエラーを吐かずに終了すればテストが成功扱いになるという、とてもシンプルなものです。

public class ScenarioTest
{
    [UnityTest]
    public IEnumerator Run()
    {
        Debug.Log("テスト開始");

        // シナリオの再生処理
        yield return RunScenario();

        Debug.Log("最後までいったのでテスト成功!!");
    }
}

これはUnityエディタから実行できるようになっており、実行すると自動的にUnityがPlay状態になり、直後にコルーチンを実行する仕組みとなっています。

test.gif

そしてもちろん、CommandLineからの実行にも対応しています。完璧ですね。

Unity.exe -projectPath <projectPath> -batchmode -runTests -testPlatform playmode -testResults <resultPath>

 
これから紹介するTipsは、このTest RunnerのPlay Modeテストを行う際のTipsとなります。

Tips

1. Play Modeテストで使うAttributeとその注意点

Test Runnerを使う際に覚えておきたいAttributeがあります。
また、Play Modeテストにおいて注意すべき点もあるので、それも交えて紹介します。

テストメソッド用

  • [UnityTest]
    • テストしたいコルーチンに必ず付ける必要がある
  • [TestCase]
    • テストケースを定義する
  • [TestCaseSource]
    • テストケースをリストで定義する(動的に定義することができる)
  • [Timeout]
    • テストのタイムアウト時間を指定する
public class ScenarioTest
{
    // テストしたいコルーチンに必ず付ける必要がある
    [UnityTest]
    public IEnumerator Run()
    {
        yield return RunScenario();
    }


    // コルーチンに引数として値を渡すことができる(複数指定可)
    // この例だと、RunWithTestCase("file_0001")、RunWithTestCase("file_0002")として扱われる
    // 【注意】 コルーチンに[TestCase]を使う場合、ExpectedResult = null を記述しないとエラーになる
    [TestCase("file_0001", ExpectedResult = null)]
    [TestCase("file_0002", ExpectedResult = null)]
    [UnityTest]
    public IEnumerator RunWithTestCase(string file)
    {
        yield return RunScenarioWithFile(file);
    }



    // [TestCaseSource]に渡すリスト
    public static IEnumerable FILES
    {
        get
        {
            foreach (var path in Directory.GetFiles("<path>"))
            {
                //  【注意】 [TestCase]同様、Returns(null) を記述しないとエラーになる
                yield return new TestCaseData(path).Returns(null);
            }
        }
    }
    // [TestCase]をリストで管理できるようにしたもの(動的に定義したい場合などで有用)
    // 各要素ごとを渡したテストとして扱われる
    // Directory.GetFilesから取得できる文字列がfile_0001、file_0002とすると、
    // RunWithTestCaseSource("file_0001")、RunWithTestCaseSource("file_0002")となる
    [TestCaseSource("FILES")]
    [UnityTest]
    public IEnumerator RunWithTestCaseSource(string file)
    {
        yield return RunScenarioWithFile(file);
    }



    // テストのタイムアウト時間を指定する
    // デフォルトだと30秒
    // ms単位で指定(この例だと5分になる)
    [Timeout(300000)]
    [UnityTest]
    public IEnumerator RunWithTimeout()
    {
        yield return RunScenario();
    }
}

ちなみにUnityエディタ上ではこう見えています。

gui.png

コールバック

  • [OneTimeSetUp]
    • クラス内で最初のテストが実行される前に一度だけ呼ばれる
  • [SetUp]
    • 各テストの最初に呼ばれる
  • [TearDown]
    • 各テストの最後に呼ばれる(テストが失敗しても呼ばれる)
  • [OneTimeTearDown]
    • クラス内で最後のテストが実行された後に一度だけ呼ばれる
public class ScenarioTest
{
    // クラス内で最初のテストが実行される前に一度だけ呼ばれる
    [OneTimeSetUp]
    public void OneTimeSetUp()
    {
        Debug.Log("テスト開始");
    }


    // 各テストの最初に呼ばれる
    [SetUp]
    public void SetUp()
    {
        Debug.Log("file_0001 のテスト開始");
    }


    // 各テストの最後に呼ばれる(テストが失敗しても呼ばれる)
    [TearDown]
    public void TearDown()
    {
        Debug.Log("file_0001 のテスト終了");
    }


    // クラス内で最後のテストが実行された後に一度だけ呼ばれる
    [OneTimeTearDown]
    public void OneTimeTearDown()
    {
        Debug.Log("テスト終了");
    }
}

なお、Test RunnerにはNUnitと呼ばれるテストフレームワークが使われているため、もっと知りたい方はNUnitのドキュメントを参照ください。

2. テスト時間を短縮する

Play Modeテストでは、UnityをPlay状態にしてテストを実行するため、最速でも実プレイと同じ速度しか出ません。

そこで、ゲーム内速度やFPSを上げることでスピードUPを図ります。

public class ScenarioTest
{
    // クラス内で最初のテストが実行される前に一度だけ呼ばれる
    [OneTimeSetUp]
    public void OneTimeSetUp()
    {
        // テスト時間短縮のために高速化する
        Application.targetFrameRate = 60;
        Time.timeScale = 2.0f;
    }
}

これだけで、テスト時間を大幅に削減することができます。
Play Modeテストならではの手法ですね。

3. 手軽にUIテストをできるようにする

シナリオの動作チェックのようなUIテストを行う場合、画面タップなどのUIの操作が必要となります。

ただ、これを自前で実装するのは手間になるため、すでにあるUnity UI Test Automation Frameworkというフレームワークを使用することをオススメします。

これを使えば、

public class ScenarioTest : UITest
{
    [UnityTest]
    public IEnumerator Run()
    {
        // targetという名前のボタンを押す
        yield return Press("target");

        // Scenarioシーンをロードする
        yield return LoadScene("Scenario");

        // targetというオブジェクトが登場するまで待機する
        yield return WaitFor(new ObjectAppeared("target"));
    }
}

といったことが簡単にできるようになります。

4. テストの失敗をハンドリングする

テストコードを書いていて、テストが失敗した場合のみ特定の処理をしたい、ということがあると思います。

もし、[TearDown]で指定したメソッドに引数として情報が渡ってきたり、テスト失敗時に呼ばれるコールバックなどがあればよかったのですが、NUnitのドキュメントを見た限りなさそうでした。

ではどうするか。
Play Modeテストはエラーログが出れば失敗となるので、Unityのログメッセージを取得することでハンドリングできるようになります。

public class ScenarioTest
{
    private List<string> _errorLogs = new List<string>();

    // クラス内で最初のテストが実行される前に一度だけ呼ばれる
    [OneTimeSetUp]
    public void OneTimeSetUp()
    {
        // ログを残す
        Application.logMessageReceived += Log;
    }

    public void Log(string logString, string stackTrace, LogType type)
    {
        // エラーログを保持しておく
        if (type == LogType.Error || type == LogType.Exception)
        {
            _errorLogs.Add($"{logString}\n{stackTrace}\n");
        }
    }

    [UnityTest]
    public IEnumerator Run()
    {
        // 各テストの頭でクリアする
        _errorLogs.Clear();

        yield return RunScenario();
    }

    // 各テストの最後に呼ばれる(テストが失敗しても呼ばれる)
    [TearDown]
    public void TearDown()
    {
        // エラーログがあるならテスト失敗
        if (_errorLogs.Count > 0)
        {
            Debug.Log("テスト失敗");
            Debug.Log(string.Join("\n", _errorLogs.ToArray()));
        }
    }
}

こうすれば、失敗時のみ処理をすることができます。

5. コード側からTest Runnerを制御する

Test RunnerがUnityエディタやCommandLineから実行できる術を用意しているとしても、やはり自前で拡張したい時はあります。

そういった場合、コード側からTest Runnerを制御することになると思いますが、実はこれがかなり面倒です。
理由は単純。Test RunnerのAPIが公開されていないからです。

そのため、リフレクションを使って無理やり実行することになります。

public class ScenarioTestCommand
{
    public static void Execute()
    {
        // テスト情報を追加する
        var engineAssembly = Assembly.Load("UnityEngine.TestRunner");
        var testFilterType = engineAssembly.GetType("UnityEngine.TestTools.TestRunner.GUI.TestRunnerFilter");
        var testFilter = Activator.CreateInstance(testFilterType);
        // テストの名前を使って実行する
        // 実行するテスト名(メソッド名)は名前空間とクラス名も含めること
        var testNamesField = testFilterType.GetField("testNames");
        testNamesField.SetValue(testFilter, new string[] { "ScenarioTest.Run(\"file_0001\")", "ScenarioTest.Run(\"file_0002\")" });

        // Test Runnerを実行できるクラスを参照する
        var editorAssembly = Assembly.Load("UnityEditor.TestRunner");
        var runnerWindowType = editorAssembly.GetType("UnityEditor.TestTools.TestRunner.TestRunnerWindow");
        var runnerWindow = runnerWindowType.GetField("s_Instance", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
        var listGUIField = runnerWindowType.GetField("m_PlayModeTestListGUI", BindingFlags.Instance | BindingFlags.NonPublic);
        var listGUI = runnerWindow != null ? listGUIField.GetValue(runnerWindow) : Activator.CreateInstance(listGUIField.FieldType);

        // Test Runnerを実行する
        var runMethod = listGUIField.FieldType.GetMethod("RunTests", BindingFlags.Instance | BindingFlags.NonPublic);
        runMethod.Invoke(listGUI, new object[] { testFilter });
    }
}

正直「何やっているんだ、このコードは」状態になると思うので、dnSpyなどのデコンパイラを使ってTestRunner.dllの中身を見ることをオススメします。

6. テストの失敗が後続のテストにまで影響を与えないようにする

Play Modeテストでは、複数のテストを実行した場合も、同じPlay状態でテストを実行します。
そのため、変数などで状態を持っていれば、それは後続のテストにも引き継がれることになります。

この特性は、途中のテストが失敗した時に厄介で、

  1. テストAが失敗する
  2. テストAが途中で失敗したので、途中の状態が保持されたままになる
  3. 次のテストBに移る
  4. 想定しない状態が保持されていることで、問題ないはずのテストBが落ちてしまう

ということが起きてしまいます。

fail.png

これに対処するには、各テストが正しく初期化された状態で実行される必要があります。
ただ、もしstaticやDontDestroyOnLoadをよく使っているプロジェクトであれば、これは困難を極めます。

シーンの再ロードなどでは初期化できず、しらみつぶしに初期化されていないところを探していくしかないからです。
自分が現在開発に携わっているゲームも同じ状況で、初めはしらみつぶしに探していましたが、途方もなく途中で断念しました。

打開策はないか…そう思案して、考えついた最強のTips(苦肉の策)、それが

一旦UnityのPlay状態を止めて再度Play状態にすれば、事実上初期化されたことと同義になる!!(スマホのタスクキルと同じ)

というものでした。

実際のコードを見てみましょう。

public class ScenarioTest
{
    private static object _testFilter = null;

    private List<string> _errorLogs = new List<string>();
    private List<string> _runTestNames = new List<string>();

    // クラス内で最初のテストが実行される前に一度だけ呼ばれる
    [OneTimeSetUp]
    public void OneTimeSetUp()
    {
        // 【注意】 この例では、Logメソッドを省略
        Application.logMessageReceived += Log;

        // Restartするため、テスト情報を保持しておく
        var assembly = Assembly.Load("UnityEngine.TestRunner");
        var controllerType = assembly.GetType("UnityEngine.TestTools.TestRunner.PlaymodeTestsController");
        var controllerObject = GameObject.Find("Code-based tests runner");
        var controller = controllerObject.GetComponent(controllerType);
        var settingsField = controllerType.GetField("settings");
        var settings = settingsField.GetValue(controller);
        var settingsType = assembly.GetType("UnityEngine.TestTools.TestRunner.PlaymodeTestsControllerSettings");
        var filterField = settingsType.GetField("filter");
        _testFilter = filterField.GetValue(settings);
    }

    // 【注意】 この例では、FILES変数の宣言は省略
    [TestCaseSource("FILES")]
    [UnityTest]
    public IEnumerator Run(string file)
    {
        // 前回のテストが失敗していたら、UnityのPlay状態を止める
        if (_errorLogs.Count > 0 && _testFilter != null)
        {
            // 【注意】 テスト情報の更新処理(今回は名前で更新しているが、カテゴリーなら別の処理が必要)
            var assembly = Assembly.Load("UnityEngine.TestRunner");
            var testFilterType = assembly.GetType("UnityEngine.TestTools.TestRunner.GUI.TestRunnerFilter");
            var testNamesField = testFilterType.GetField("testNames");
            var testNames = (string[])testNamesField.GetValue(_testFilter);
            // すでに実行しているテストは削除する
            testNames = testNames.Where(testName => !_runTestNames.Any(runTestName => testName.Contains(runTestName))).ToArray();
            testNamesField.SetValue(_testFilter, testNames);

            // Unityを止めて、Test Runnerを再び実行する
            EditorApplication.isPlaying = false;
            EditorApplication.update += OnRestart;
        }

        _errorLogs.Clear();
        _runTestNames.Add(file);

        yield return RunScenarioWithFile(file);
    }

    private static void OnRestart()
    {
        Restart(_testFilter);
    }

    // コード側からTest Runnerを実行する
    private static void Restart(object testFilter)
    {
        var assembly = Assembly.Load("UnityEditor.TestRunner");
        var runnerWindowType = assembly.GetType("UnityEditor.TestTools.TestRunner.TestRunnerWindow");
        var runnerWindow = runnerWindowType.GetField("s_Instance", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
        var listGUIField = runnerWindowType.GetField("m_PlayModeTestListGUI", BindingFlags.Instance | BindingFlags.NonPublic);
        var listGUI = runnerWindow != null ? listGUIField.GetValue(runnerWindow) : Activator.CreateInstance(listGUIField.FieldType);
        var runMethod = listGUIField.FieldType.GetMethod("RunTests", BindingFlags.Instance | BindingFlags.NonPublic);
        runMethod.Invoke(listGUI, new object[] { testFilter });
        EditorApplication.update -= OnRestart;
    }
}

やっていることは、以下の通りです。

  1. テスト開始時にTest Runnerからテスト情報を抜き出す
  2. 各テスト開始時、前回のテストが失敗したか確認する
  3. テストに失敗していれば、すでにテスト済みのものを、開始時の保持しておいたテスト情報から削除する
  4. UnityのPlay状態を止め、更新したテスト情報をもとにTest Runnerを再実行する

Test Runnerを実行すれば、UnityはPlay状態になるため、事実上の初期化が完成です。

注意として、テスト情報が名前ではなくカテゴリーで管理されていることもあるため、その際は別の更新処理が必要です。
 
 
苦肉の策ではありますが、これで全シナリオのテストが可能となりました。

ただ一点、この方法で問題になることがありました。
次のTipsに移ります。

7. CommandLineからTest Runnerを実行する

通常のケース

冒頭でも記述した、Test Runnerで用意されているコマンドで実行可能です。

Unity.exe -projectPath <projectPath> -batchmode -runTests -testPlatform playmode -testResults <resultPath>

これに-quitが指定されていないことにお気づきでしょうか?

実は、このコマンドを実行すると、テスト終了時にUnityが自動的に終了するようになっています。
逆に、-quitを付けると失敗してしまうためご注意ください。

特殊なケース

6. テストの失敗が後続のテストにまで影響を与えないようにする」で紹介した方法だと、通常のケースと同じコマンドを実行しても上手くいきません。

問題は2つあります。
解決策とともに、一つずつ紹介していきます。

Unityが終了しない問題

通常のケースにて紹介した、-quitを指定せずとも、テスト終了時にUnityが自動的に終了する機能。
本来であれば問題がないこの挙動ですが、先ほど紹介した一旦Play状態を止める方法だと厄介です。

というのも、一旦Play状態を止めるという動作は、Test Runnerが不正終了したとみなされ、その機能が働かなくなってしまうからです。

このままでは、CommandLineからの実行時、途中でテストに失敗すると、Unityが終了されません。

そこで、batchmodeでの実行であれば、全テスト終了時にUnityを終了するようにしました。

public class ScenarioTest
{
    // クラス内で最後のテストが実行された後に一度だけ呼ばれる
    [OneTimeTearDown]
    public void OneTimeTearDown()
    {
        // batchmodeなら終了させる
        if (Environment.CommandLine.Contains("-batchmode"))
        {
            // エラー扱いにするため、1以上を返しても良い
            EditorApplication.Exit(0);
        }
    }
}

途中でテストが失敗しPlay状態が止められても、[OneTimeTearDown]は呼ばれないことが肝ですね。
(複数のクラスが存在する場合は、他のクラスのテストが終了しているかのチェックが必要になります)

無限に同じテストを実行され続ける問題

もしこの記事のコードをそのまま使っている場合、無限に同じテストが実行され続けてしまいます。

これは、

Unity.exe -projectPath <projectPath> -batchmode -runTests -testPlatform playmode -testResults <resultPath>

このコマンドで作成されるテスト情報は名前で管理されていないため、記事のコードのように、すでに実行したテストの名前を削除しても意味がないためです。

もし、この記事のコードを使い回したい場合は、名前で管理されているテスト情報を作成しTest Runnerで実行するメソッドを、CommandLineから呼ぶことをオススメします。

Unity.exe -projectPath <projectPath> -batchmode -executeMethod ScenarioTestCommand.Execute

実行しているScenarioTestCommand.Executeメソッドは、「5. コード側からTest Runnerを制御する」で紹介しているコードを参照してください。
こちらのサンプルコードは、名前で管理されているテスト情報を作成し、実行しています。

まとめ

自分の開発しているゲームでは、この全シナリオ動作テストを毎朝走らせ、失敗すればslackに通知するようにしています。

今回テストコードを入れたことで、シナリオデータの不具合が消えただけでなく、シナリオ再生用のコードを変更するハードルが下がったことも嬉しいポイントです。

ゲーム開発だとテストコードを書く機会も少なかったですが、この機会に今後も積極的に取り組んでいきたいと思います。

Twitter: @yukiarrr

その他参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Razor構文とbootstrap4でのメモ

Razor構文でボタンタグにtype="button"と記述するとPOSTできない

index.cshtml
<button asp-controller="hoge" asp-action="hoge" name="bt" value="sa">

type=buttonと書くとPOSTできないのでした

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【.NetCore】プロジェクト作成時のエラー「'LayoutAttribute' が名前空間 'Microsoft.AspNetCore.Components' に存在しません」の対応方法【Blazor】

.NetCore3.0のBlazorアプリはフロントエンドの処理をC#で書ける画期的な技術。
とりあえず試してみようと思い新規でプロジェクトを立ち上げたらいいきなり躓いたので解決方法を載せておく。

新規で作ると実行できない

新規プロジェクトで作成して、そのまま実行すると以下のエラーが多数でて実行ができない場合がある。

エラー   CS0234  型または名前空間の名前 'LayoutAttribute' が名前空間 'Microsoft.AspNetCore.Components' に存在しません (アセンブリ参照があることを確認してください)

足りないパッケージを追加してあげることで解消します。

ClientプロジェクトにNugetからライブラリを追加する

Blazor.DevServerを追加
devServer.PNG

Blazor.HttpClientを追加
BlazorHttp.PNG

どっちもプレリリース版なので「プレリリースを含める」にチェックを入れないと検索に引っかからないので注意!
Server、Client共にデフォルトで入っている他のパッケージも更新が来ていたら最新にしておこう。

_Imports.razorを編集

_Imorts.razor
@using System.Net.Http
@using Microsoft.AspNetCore.Components.Forms
@*@using Microsoft.AspNetCore.Components.Layouts*@ 3行目をコメントアウト
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.JSInterop
@using WebApplication27.Client
@using WebApplication27.Client.Shared

Microsoft.AspNetCore.Components.Layoutsのusingがいらないので消すかコメントアウトします。

FetchDara.razorのオーバーライドメソッドの名前変更

_Imorts.razor
    //protected override async Task OnInitAsync()
    protected override async Task OnInitializedAsync()
    {
        forecasts = await Http.GetJsonAsync<WeatherForecast[]>("api/SampleData/WeatherForecasts");
    }

メソッド名をOnInitAsyncからOnInitializedAsyncに変更する。

これで実行してみよう、動くはず……?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

テスト

テスト投稿です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

.NET Coreで オブジェクト を JSON に変換する

概要

.NET Core でオブジェクト (POCO) を JSON に変換する方法です。

環境

  • Windows10
  • Visual Studio 2019
  • .NET Core 2.2

コードサンプル

サンプルコードを Github にアップしています。
https://github.com/tYoshiyuki/dotnet-core-convert-json

解説

変換対象のクラスを準備します。
ポイントとして変換対象となるクラスに DataContract を付与します。
また、変換対象となるメンバーに DataMember を付与します。
JSONのキー名は通常小文字になるかと思いますので、Name でキー名を設定します。

[DataContract]
public class SampleModel
{
    [DataMember(Name = "intValue")]
    public int IntValue { get; set; }

    [DataMember(Name = "stringValue")]
    public string StringValue { get; set; }

    [DataMember(Name = "dateTimeValue")]
    public DateTime DateTimeValue { get; set; }

    [DataMember(Name = "listValue")]
    public List<string> ListtValue { get; set; }

    [DataMember(Name = "mapValue")]
    public Dictionary<string, string> MapValue { get; set; }

    [DataMember(Name = "subSampleModel")]
    public SubSampleModel SubSampleModel { get; set; }
}

public class SubSampleModel
{
    [DataMember(Name = "intValue")]
    public int IntValue { get; set; }

    [DataMember(Name = "stringValue")]
    public string StringValue { get; set; }
}

データの変換には System.Runtime.Serialization.Json の DataContractJsonSerializer を利用します。
2019-08-20_001733.png

var settings = new DataContractJsonSerializerSettings
{
    UseSimpleDictionaryFormat = true,
    DateTimeFormat = new DateTimeFormat("yyyy-MM-dd'T'HH:mm:ss.fff'Z'"),
};

var model = new SampleModel
{
    IntValue = 10,
    StringValue = "ABC",
    DateTimeValue = DateTime.Now,
    ListtValue = new List<string> { "First", "Second", "Third" },
    MapValue = new Dictionary<string, string>() { { "Key01", "Val01" }, { "Key02", "Val02" } },
    SubSampleModel = new SubSampleModel { IntValue = 20, StringValue = "DEF" }
};

using (MemoryStream ms = new MemoryStream())
{
    var serializer = new DataContractJsonSerializer(typeof(SampleModel), settings);
    serializer.WriteObject(ms, model);
    Console.WriteLine(Encoding.UTF8.GetString(ms.ToArray()));
}

DataContractJsonSerializerSettings の UseSimpleDictionaryFormat = true とすることで、
Dictionary を キー : 値 の形式で出力する事が出来ます。
また、DateTimeFormat を設定することで日時フォーマットの出力形式を変更する事ができます。


{
    "dateTimeValue": "2019-08-20T00:32:57.420Z",
    "intValue": 10,
    "listValue": [
        "First",
        "Second",
        "Third"
    ],
    "mapValue": {
        "Key01": "Val01",
        "Key02": "Val02"
    },
    "stringValue": "ABC",
    "subSampleModel": {
        "intValue": 20,
        "stringValue": "DEF"
    }
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む