おがさわらなるひこのオープンソースとかプログラミングとか印刷技術とか

おがさわらなるひこ @naru0ga が技術系で興味を持ったりなんだりしたことをたまーに書くブログです。最近はてなダイアリー放置しすぎて記事書くたびにはてな記法忘れるのではてなブログに移行しました。

クリエイティブ・コモンズ・ライセンス
特に断りがない場合は、本ブログの筆者によるコンテンツは クリエイティブ・コモンズ 表示 - 継承 4.0 国際 ライセンスの下に提供されています。

短期集中連載? LibreOfficeをWindowsで開発してみよう:補足その①ユニットテストについて

GWの間にLibO開発できる環境をWindowsで作ろうの連載をやりましたが、あれからようやっと修正パッチ投げるところまでやりましたので、その中で気づいたことなど。

過去記事:第1回 第2回 第3回 第4回 第5回

なお再度お断りですが、この一連の記事はLibreOfficeの開発について解説しようというものではないです。

というのは、解説記事をかけるほど自分詳しくないってことと、 LibreOfficeのように開発が活発なプロジェクトにおいて、 開発情報は将来において変わりうるので解説の寿命がそんなに長くないってことです*1

あくまでも、Wikiにある公式情報:

wiki.documentfoundation.org

を参照しつつ、私の記事は、 あーなるほどこんなところで引っかかったりするのかーみたいな一種のドキュメンタリーとして楽しんでいただけたら。

あともう一つ、第5回で、 問題のどこを直せばいいかというのはわかったつもりだけど、 どう直せばいいのか(仕様変更すべきなのか)がわかってないみたいに書きました。 それについては、メーリングリストに質問投げまして、 仕様変更になるかもだけど直した方がよかろうということに賛同いただきまして決着。 よかったよかった。

さてさて。前置き長くてすみません。本題。

実際に開発を進めていくにあたって、以下3点少し悩んだので。

  1. ユニットテストをどう書いてどう実行するか
  2. パッチをどうやって送るか
  3. ヘルプの更新の仕方

と、書いてたらむちゃくちゃ長くなったので、この記事はまずは①だけについて。②③は別記事にします。

1. ユニットテストをどう書いてどう実行するか

LibreOfficeのような、日々膨大な変更があって*2 リリース頻度も高いソフトウェアには、自動テストが欠かせません。

ロジックを何らか変えたら、 当然、テストを実行してリグレッション*3がないかどうかを確認し、 場合によっては、新たに実装したロジックが適切に動作するかどうかのテストを書かなくてはいけません。

LibreOfficeには自動テストのしくみがいくつかありますが、基本となるのはC++なロジックをC++なテストコードで確認するユニットテスト。 例によって公式Wikiに記載があります。

wiki.documentfoundation.org

さて。「どう書いて」については、この記事ではお伝えできる情報あんまり情報がないです……。 今回のように既存のロジックを変えましたよという場合、そのロジックに関するテストを探して、 必要ならそいつに書き足す……ってな感じでしょうね。

で、前回同様「DBNum」でプロジェクト丸ごと検索かけて、今回は関係しそうなテストは二つ。

一つは sc/qa/unit/subsequent_export-test.cxx の以下の部分:

void ScExportTest::testNatNumInNumberFormatXLSX()
{
    ScDocShellRef xDocSh = loadDoc("tdf79398_NatNum5.", FORMAT_ODS);
    CPPUNIT_ASSERT( xDocSh.is() );
    xDocSh = saveAndReload( &(*xDocSh), FORMAT_XLSX);  // Convert [NatNum5] to [DBNum2] in Chinese
    CPPUNIT_ASSERT( xDocSh.is() );

    xmlDocUniquePtr pDoc = XPathHelper::parseExport2(*this, *xDocSh, m_xSFactory, "xl/styles.xml", FORMAT_XLSX);
    CPPUNIT_ASSERT(pDoc);

    assertXPath(pDoc, "/x:styleSheet/x:numFmts/x:numFmt[3]", "formatCode", "[DBNum2][$-804]General;[RED][DBNum2][$-804]General");

    xDocSh->DoClose();
}

こいつは:

  • 名前の通りエクスポートのときに NatNum -> DBNum の変換がちゃんとおこわなれるかのテストで
  • あらかじめ用意したODSファイルをXLSX形式で保存しなおして
  • 出てる内容のXPathで正しさを確認する

ってものなのですけど…… うーん、データファイル作るの少し面倒だし、 XPathの書き方もよくわかってないので*4 こいつはいったん置きます。

もう一つは、 svl/qa/unit/svl.cxx の以下のコード。

void Test::testUserDefinedNumberFormats()
{
    ...
    {  // tdf#79399 tdf#101462 Native Number Formats
        sCode = "[NatNum5][$-0404]General\\ ";
        // Chinese upper case number characters for 120
        sExpected = u"\u58F9\u4F70\u8CB3\u62FE ";
        checkPreviewString(aFormatter, sCode, 120, eLang, sExpected);
        sCode = "[DBNum2][$-0404]General\\ ";
        checkPreviewString(aFormatter, sCode, 120, eLang, sExpected);
    ...

こいつはセルの書式画面にて書式コードを手で入力するときの「プレビュー」をもとに、 書式が期待通りに処理されるかということを確認。お、これはわかりやすいですね。

もうちょっと読んでみますと、書式指定文字列らしい [NatNum5][$-0404]General\\、 この [$-0404] ってなんじゃろ……と思って調べたら、 ヘルプ によると、 include/lang.h にある:

#define LANGUAGE_CHINESE_TRADITIONAL        LanguageType(0x0404)
...
#define LANGUAGE_JAPANESE                   LanguageType(0x0411)
...
#define LANGUAGE_KOREAN                     LanguageType(0x0412)

みたいですね。 今回の場合、 セル内の言語によって結果が変わることをテストしないといけないのでこれは大変に都合がよろしい。

で、 sExpected = u"\u58F9\u4F70\u8CB3\u62FE "; ですが、 これは適当なツール*5 UTF-16 デコードすると 壹佰貳拾 となって、 あーなるほど、checkPreviewString() の第3引数に渡してるやつの NatNum5 = DBNum2 の期待値なのですね。

ということでこのロジックはままコピれるみたいなので、 別個こんな感じでまとめました。ちょっと長いけど。

    { // tdf#130193 tdf#130140 Native Number Formats mapping for Chinese (Traditonal), Japanese, Korean
        // -- Traditional Chinese: DBNum1 -> NatNum4, DBNum2 -> NatNum5, DBnum3 -> NatNum6

        // DBNum1 -> NatNum4: Chinese lower case text for 123456789
        // 一億二千三百四十五萬六千七百八十九
        sExpected = u"\u4e00\u5104\u4e8c\u5343\u4e09\u767e\u56db\u5341\u4e94\u842c\u516d\u5343"
                    u"\u4e03\u767e\u516b\u5341\u4e5d ";
        sCode = "[NatNum4][$-0404]General\\ ";
        checkPreviewString(aFormatter, sCode, 123456789, eLang, sExpected);
        sCode = "[DBNum1][$-0404]General\\ ";
        checkPreviewString(aFormatter, sCode, 123456789, eLang, sExpected);

        // DBNum2 -> NatNum5: Chinese upper case text
        // 壹億貳仟參佰肆拾伍萬陸仟柒佰捌拾玖
        sExpected = u"\u58f9\u5104\u8cb3\u4edf\u53c3\u4f70\u8086\u62fe\u4f0d\u842c\u9678\u4edf"
                    u"\u67d2\u4f70\u634c\u62fe\u7396 ";
        sCode = "[NatNum5][$-0404]General\\ ";
        checkPreviewString(aFormatter, sCode, 123456789, eLang, sExpected);
        sCode = "[DBNum2][$-0404]General\\ ";
        checkPreviewString(aFormatter, sCode, 123456789, eLang, sExpected);

        // DBNum3 -> NatNum6: fullwidth text
        // 1億2千3百4十5万6千7百8十9
        sExpected = u"\uff11\u5104\uff12\u5343\uff13\u767e\uff14\u5341\uff15\u842c\uff16\u5343"
                    u"\uff17\u767e\uff18\u5341\uff19 ";
        sCode = "[NatNum6][$-0404]General\\ ";
        checkPreviewString(aFormatter, sCode, 123456789, eLang, sExpected);
        sCode = "[DBNum3][$-0404]General\\ ";
        checkPreviewString(aFormatter, sCode, 123456789, eLang, sExpected);

        // -- Japanese: DBNum1 -> NatNum4, DBNum2 -> NatNum5, DBnum3 -> NatNum3

        // DBNum1 -> NatNum4: Japanese modern long Kanji text for 123456789
        // 一億二千三百四十五万六千七百八十九
        sExpected = u"\u4e00\u5104\u4e8c\u5343\u4e09\u767e\u56db\u5341\u4e94\u4e07\u516d\u5343"
                    u"\u4e03\u767e\u516b\u5341\u4e5d ";
        sCode = "[NatNum4][$-0411]General\\ ";
        checkPreviewString(aFormatter, sCode, 123456789, eLang, sExpected);
        sCode = "[DBNum1][$-0411]General\\ ";
        checkPreviewString(aFormatter, sCode, 123456789, eLang, sExpected);

        // DBNum2 -> NatNum5: traditional long Kanji text
        // 壱億弐阡参百四拾伍萬六阡七百八拾九
        sExpected = u"\u58f1\u5104\u5f10\u9621\u53c2\u767e\u56db\u62fe\u4f0d\u842c\u516d\u9621"
                    u"\u4e03\u767e\u516b\u62fe\u4e5d ";
        sCode = "[NatNum5][$-0411]General\\ ";
        checkPreviewString(aFormatter, sCode, 123456789, eLang, sExpected);
        sCode = "[DBNum2][$-0411]General\\ ";
        checkPreviewString(aFormatter, sCode, 123456789, eLang, sExpected);

        // DBNum3 -> NatNum3: fullwidth Arabic digits
        // 123456789
        sExpected = u"\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19 ";
        sCode = "[NatNum3][$-0411]General\\ ";
        checkPreviewString(aFormatter, sCode, 123456789, eLang, sExpected);
        sCode = "[DBNum3][$-0411]General\\ ";
        checkPreviewString(aFormatter, sCode, 123456789, eLang, sExpected);

        // -- Korean: DBNum1 -> NatNum1, DBNum2 -> NatNum2, DBNum3 -> NatNum4, DBNum4 -> NatNum9

        // DBNum1 -> NatNum1: Korean lower case characters
        // 一二三四五六七八九
        sExpected = u"\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d ";
        sCode = "[NatNum1][$-0412]General\\ ";
        checkPreviewString(aFormatter, sCode, 123456789, eLang, sExpected);
        sCode = "[DBNum1][$-0412]General\\ ";
        checkPreviewString(aFormatter, sCode, 123456789, eLang, sExpected);

        // DBNum2 -> NatNum2: Korean upper case characters
        // 壹貳參四伍六七八九
        sExpected = u"\u58f9\u8cb3\u53c3\u56db\u4f0d\u516d\u4e03\u516b\u4e5d ";
        sCode = "[NatNum2][$-0412]General\\ ";
        checkPreviewString(aFormatter, sCode, 123456789, eLang, sExpected);
        sCode = "[DBNum2][$-0412]General\\ ";
        checkPreviewString(aFormatter, sCode, 123456789, eLang, sExpected);

        // DBNum3 -> NatNum3: fullwidth Arabic digits
        // 123456789
        sExpected = u"\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19 ";
        sCode = "[NatNum3][$-0412]General\\ ";
        checkPreviewString(aFormatter, sCode, 123456789, eLang, sExpected);
        sCode = "[DBNum3][$-0412]General\\ ";
        checkPreviewString(aFormatter, sCode, 123456789, eLang, sExpected);

        // DBNum4 -> NatNum9: Hangul characters
        // 일이삼사오육칠팔구
        sExpected = u"\uc77c\uc774\uc0bc\uc0ac\uc624\uc721\uce60\ud314\uad6c ";
        sCode = "[NatNum9][$-0412]General\\ ";
        checkPreviewString(aFormatter, sCode, 123456789, eLang, sExpected);
        sCode = "[DBNum4][$-0412]General\\ ";
        checkPreviewString(aFormatter, sCode, 123456789, eLang, sExpected);
    }

さて、実行について。

さきのページにもありました通り、全部のテストを実行するには、

make check

するだけ。このときウィルスチェックがONだと失敗することがあるのでOFFっとくのがいいみたいです*6

ただ、LibOのユニットテストはとても膨大なので、毎度毎度全部のテストを流すのはホネが折れます。 なので狙ったテストだけ流す方法もあります。

 make CppunitTest_svl_qa_cppunit CPPUNIT_TEST_NAME="testUserDefinedNumberFormats"

たとえばこんな感じ。 今回書いたテストは svl/qa/unit/svl.cxx 以下にあるので、makeのターゲットは "svl_qa" となるというわけですね。 ……いや、正直にいうと、たぶん ls svl/CppunitTest*.mk してそれっぽいものを探しただけな気がします。

なお、事前にモジュールがあるフォルダーに cd しておくと、ちょっとだけテストの起動が早くなりますね。

で、失敗したとき。このときもとてもよくできてまして、失敗するとこんな感じのメッセージが出ます。

C:/cygwin/home/naruhiko/lode/dev/core/svl/qa/unit/svl.cxx:437:`anonymous namespace'::Test::testUserDefinedNumberFormats
equality assertion failed
- Expected: 一億二千三百四十五萬六千七百八十九
- Actual  : 一千二百三十四萬五千六百七十八

`anonymous namespace'::Test::testUserDefinedNumberFormats finished in: 736ms
C:/cygwin/home/naruhiko/lode/dev/core/svl/qa/unit/svl.cxx(437) : error : Assertion
Test name: `anonymous namespace'::Test::testUserDefinedNumberFormats
equality assertion failed
- Expected: 一億二千三百四十五萬六千七百八十九
- Actual  : 一千二百三十四萬五千六百七十八

Failures !!!
Run: 1   Failure total: 1   Failures: 1   Errors: 0
warn:unotools.config:1836:23384:unotools/source/config/configmgr.cxx:140: ConfigManager not empty

Error: a unit test failed, please do one of:
make CppunitTest_svl_qa_cppunit CPPUNITTRACE=TRUE # which is a shortcut for the following line
make CppunitTest_svl_qa_cppunit CPPUNITTRACE="'C:/Program Files (x86)/Microsoft Visual Studio/2019/Community/Common7/IDE/devenv.exe' /debugexe" # for interactive debugging in Visual Studio
make CppunitTest_svl_qa_cppunit CPPUNITTRACE="drmemory -free_max_frames 20" # for memory checking (install Dr.Memory first, and put it to your PATH)

You can limit the execution to just one particular test by:

make CppunitTest_svl_qa_cppunit CPPUNIT_TEST_NAME="testXYZ" ...above mentioned params...

そう、ここに書いてある通り、

make CppunitTest_svl_qa_cppunit CPPUNITTRACE=TRUE

ってやると、Visual Studioが立ち上がって、この中でユニットテスト(と、もちろん、その中で呼び出されるLibOのコード)をデバッグできるんですね。 当然 ‘CPPUNIT_TEST_NAME` との併用も可能です。

これで、新規に作ったテストがちゃんと通るようになったら、あらためてユニットテストもgitにコミットして。

コミットしたコードで正しくテストが全部通るかどうか、 make check をもう一度やっておきましょう。 わたくしはこの工程をさぼったため、先に示した svl/qa/unit/svl.cxx のテストの実行がCIでfailしてあわわわわ……ってなりました。恥ずかしい*7

さてさて長くなりました。gitにコミットした内容をもとに、いよいよパッチの提出……は、次の記事にて。

*1:同様の理由で、 公式Wikiの開発情報のページ翻訳するの昔は抵抗があるんですよね……。 Readmeなんかで使ってるMediaWikiの翻訳プラグイン適用してくれないかなー。 そしたら原文更新されたパラグラフは英語になるので、うっかり古い情報を提供せずに済むので。

*2:プロジェクトの日々を知ることができるサイト https://dashboard.documentfoundation.org/ によれば一日当たりだいたい300弱のコミットがあるらしいですね。

*3:回帰ともいう。つまり、変更によって過去に動いてた機能が動かなくなったなど。

*4:今この記事を書くために再度読み直したらそんなに大変じゃないかも……。 たぶん [$-804] がExcel用の言語コードなので、生成されたXLSXをunzipして中のXML覗いて、これだけ直してあげればよさそう。 それで日本語・韓国語のテスト用ODSを作ってあげさえすれば。 レビューで指摘もらったら対応するかも。

*5:私はこいつ使いました。

*6:わたくしの場合はこのとき、masterからとってきただけのソースでも特定のテストがfailしました……この場合正しくは「どうなってるの?」とメーリングリストなりなんなりで聞くべきなのですが、わたくしはとりあえず無視で先に進みました……よくない。

*7:自分でテスト実行した後、手作業でちょっとだけ直してコミットしたコードに誤りがあったのです。