PlatformIO でユニットテストが書きたい

先日、うっかりM5Paperを買ってしまったので、肥やしにならぬよう、ちょくちょくプログラムを書いているのだが、プロジェクト内で書いた自分のクラスをテストしたいと思った時に少しハマったので、解決策を書いておく。

前提

Arduino IDEは使わない。PlatformIOを使います。ビルドがめちゃくちゃ早いので(Arduino IDEが遅いとも言う)オススメ。

本旨とズレるが、PlatformIO Coreっていうのがあったんだな。PlatformIO IDEしかないのかと思って、VSCodeで書いてた……。

testフォルダ

最初、何も考えずにプロジェクト内のtestフォルダ内に、test_hoge.cppを置いて、以下のようなコードを書いたところうまくいった。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <Arduino.h>
#include <unity.h>

void test0()
{
TEST_ASSERT_TRUE(true);
}

void test1()
{
TEST_ASSERT_FALSE(false);
}

void setup()
{
UNITY_BEGIN();
RUN_TEST(test0);
RUN_TEST(test1);
UNITY_END();
}

void loop()
{
}

しかし、ここで既にあった自分のクラスをテストしたいと思って以下のようなコードを追加したら、テストコードのビルドが通らなくなった。

1
2
3
4
5
6
7
#include "test_class.h"

void test0()
{
auto t = new TestClass();
TEST_ASSERT_TRUE(t != nullptr);
}

test_build_project_src = yes

ドキュメントを確認すると、プロジェクト設定であるplatformio.iniに、test_build_project_srcという設定があり、それを有効にしないとsrcフォルダがビルドされないらしい(test_class.hincludeフォルダ、test_class.cppsrcフォルダにあった)。

なるほど、と思い有効にしてビルドしたが、今度はsetup()loop()が二重定義になってると怒られる。当たり前だ。

lib_dir

更にドキュメントを読んでいくと、Shared Codeと言うところに、本体とテストで共有するコードはlib_dirに置けとある。

なるほどなるほど、と思いtest_class.cpplibに移動して試すがダメ。test_class.hもか?と思い試すがダメ。

ちなみに、test_build_project_srcは無効にしている。

サンプル

更にドキュメントを精査すると、サンプルを見なさいと書いてあった。

自分のプロジェクトと比較すると、libフォルダ内の構成が全く違うことに気づいたので、サンプルに合わせたところ、バッチリうまくいった。

結論

最終的なプロジェクト構成は以下のようになった。

ポイントは二つ。

  • libの中は、lib/$(好きなモジュール名)/srcに共有するコードを入れる。ヘッダも一緒に入れちゃっておk。共有ヘッダの#include<>
  • testの中は、test/$(テストケース名)にテストコードを入れる。

今回はやってないけど、テストケースの設定で、実際にデバイスに送って実行するテスト以外に、ホストPCで実行するテストとかも書けるみたい。

omnisharp-vim で .NET Framework 4.7.1 が見つからないとか言われる時の対処法

なんか毎回アップデートする度に悩んでる気がするので備忘録。

4.7.1 というバージョンは任意。
Windows だと Visual Studio とかでよしなに。

OSX の場合。

ここでは「最新の mono を入れろ」とか 「.NET Core を入れろ」と言われてますが、半分正解。ちょっと足りない。

omnisharp-vim でサーバを起動する際、 OmniSharpInstall コマンドでダウンロードした omnisharp-roslyn を使うのだが、これそのまま使うと omnisharp-roslyn に同梱されている mono コマンドを使うようになる。この mono から .NET Framework 4.7.1 が見つからないのが問題なので、これをなんとかしてやる。

具体的には

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/env bash

base_dir="$(cd "$(dirname "$0")" && pwd -P)"
bin_dir=${base_dir}/bin
etc_dir=${base_dir}/etc
omnisharp_dir=${base_dir}/omnisharp

#mono_cmd=${bin_dir}/mono
mono_cmd=/Library/Frameworks/Mono.framework/Versions/Current/bin/mono
omnisharp_cmd=${omnisharp_dir}/OmniSharp.exe
config_file=${etc_dir}/config

chmod 755 ${mono_cmd}

no_omnisharp=false

if [ "$1" = "--no-omnisharp" ]; then
shift
no_omnisharp=true
fi

export MONO_CFG_DIR=${etc_dir}
export MONO_ENV_OPTIONS="--assembly-loader=strict --config ${config_file}"

if [ "$no_omnisharp" = true ]; then
"${mono_cmd}" "$@"
else
"${mono_cmd}" "${omnisharp_cmd}" "$@"
fi

サーバ起動に使われるスクリプト ~/.cache/omnisharp-vim/omnisharp-roslyn/run を書き換えて、同梱の mono ではなく、別途インストールした mono を使うように修正するだけ。 [1]

omnisharp-roslyn の方で、パスで優先されている mono を使うようになってくれればいいんだが、まあ、このくらいで動くなら安いものか。


  1. 8行目 ↩︎

ComputeShaderでProcedural

今更ながらに ComputeShader でも触っておくかと思い、何がしたいのか考えを巡らせた挙げ句、 GeometoryShader みたいなことがしたかったというあんまり面白くない結論に達した。

Graphics.DrawProcedural() かなー?と思い、ググって漁ったが、思ったような解説が無かったので少し苦労した。みんな、「インスタンシングで大量表示!物量!」がやりたいみたいで、自分の目的とはちょっと違ったのだ。

自分がやりたかったのは、

  1. 位置のみ渡す
  2. ComputeShader でその点を中心としたポリゴンを生成
  3. 描画

というような、先ほども書いたように GeometoryShader なんである。

まずは、スクリプト。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Assertions;
using Unity.Mathematics;

public class TestComputeRenderer : MonoBehaviour
{
public ComputeShader ComputeShader;
private ComputeBuffer PreCalc;
private ComputeBuffer PostCalc;
private Material Material;
private MaterialPropertyBlock Properties;
private int KernelID;

void OnEnable()
{
if (PreCalc == null)
{
PreCalc = new ComputeBuffer(1, Marshal.SizeOf(typeof(Source)));
}
if (PostCalc == null)
{
PostCalc = new ComputeBuffer(3, Marshal.SizeOf(typeof(Output)));
}

KernelID = ComputeShader.FindKernel("CSMain");
ComputeShader.SetBuffer(KernelID, "_Source", PreCalc);
ComputeShader.SetBuffer(KernelID, "_Output", PostCalc);

if (Material == null)
{
Material = new Material(Shader.Find("TestCompute"));
Properties = new MaterialPropertyBlock();
Properties.SetBuffer("_Source", PostCalc);
}
}

void OnDisable()
{
PreCalc.Release();
PostCalc.Release();
Destroy(Material);
Material = null;
}

void LateUpdate()
{
var data = new Source[]
{
new Source(){ Position = new float3(0, 0, 0) },
};
PreCalc.SetData(data);
ComputeShader.Dispatch(KernelID, 1, 1, 1);

var bounds = new Bounds(Vector3.zero, Vector3.one * 1000);
Graphics.DrawProcedural(Material, bounds, MeshTopology.Triangles, 3, 1, null, Properties, ShadowCastingMode.Off, false, 0);
}

[StructLayout(LayoutKind.Sequential)]
public struct Source
{
public float3 Position;
}

[StructLayout(LayoutKind.Sequential)]
public struct Output
{
public float3 Position;
}
}
  • 点が一つだけ入る ComputeBuffer と、計算後の点が三つ入る ComputeBuffer を用意する
  • それぞれを、 ComputeShder 及び描画用の Material/MaterialPropertyBlock に、 .SetBuffer() する
  • 計算前の ComputeBuffer に、点のデータを .SetData() する
  • ComputeShader を .Dispatch() で起動する
  • Graphics.DrawProcedural() を呼ぶ

次に ComputeShader 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#pragma kernel CSMain

struct Source
{
float3 Position;
};

struct Output
{
float3 Position;
};

StructuredBuffer<Source> _Source;
RWStructuredBuffer<Output> _Output;

[numthreads(1, 1, 1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
_Output[id.x+0].Position = _Source[id.x].Position + float3(-1, 1, 0);
_Output[id.x+1].Position = _Source[id.x].Position + float3(1, 1, 0);
_Output[id.x+2].Position = _Source[id.x].Position + float3(0, -1, 0);
}

大したことはしてない。入ってきた点一つに対して、三つの点を出力してるだけ。

点の番号は SV_DispatchThreadID.x なのだが、この辺の説明は他を読んだ方が早いし詳しいです。

最後に描画用の Vertex/FragmentShader 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
Shader "TestCompute"
{
Properties
{
}

SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{
Cull Off
Lighting Off
BlendOp Add, Add
Blend One One, One One
ZWrite On
ZTest Always
CGPROGRAM
#pragma target 4.5
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"

struct c2v
{
float3 Position;
};

struct v2f
{
float4 vertex : SV_POSITION;
};

StructuredBuffer<c2v> _Source;

v2f vert(uint vertexID : SV_VertexID)
{
v2f o;
o.vertex = UnityObjectToClipPos(_Source[vertexID].Position);
return o;
}

fixed4 frag(v2f i) : SV_Target
{
return fixed4(1, 0, 0, 1);
}
ENDCG
}
}
}

スクリプトで .SetBuffer() したものが StructureBuffer として渡ってくるので、それをいつも通り変換して送り出すだけの簡単なお仕事。

とりあえず、最小構成はこんな感じというのが分かった。