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

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

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

jOpenDocumentを2021年にリブートしてみる その3:落ちてるテストを成功させる

naruoga.hatenablog.com

のつづき。おつむの出来がしょぼいのでXMLスキーマValidationがコケてる理由がわからないため、ほかのテストのFailを潰していきます。

幸いなことにValidatorはテスト内部でしか使っていないので、これでもアプリ側から使う分には問題ない……はず*1

落ちてるテストを拾う

でも、実はValidation以外で落ちてるテストって二つだけなんですよねえ……。

f:id:naruoga:20210716112037p:plain
落ちてるテストは日付と時刻関係だけ

しかも落ち方が……あやしい。これはテストのGlobalization対応が不十分なんじゃないか?

f:id:naruoga:20210716112213p:plain
落ち方がいかにも怪しい

DataStyleTest.testFormat() を直す

まずは簡単そうなほうから。

junit.framework.ComparisonFailure: 
Expected :0,500
Actual   :0.500

これは絶対、Locale関係だろ……と思うので、見てみましょう。

落ちてるところまでコードを引用するとこんな感じ。

    public void testFormat() throws Exception {
        final ODPackage pkg = new ODPackage(this.getClass().getResourceAsStream("/cellFormat.ods"));
        final Sheet sheet = pkg.getSpreadSheet().getSheet(0);

        // * test that the framework format as OpenOffice
        final int lastRow = sheet.getCurrentRegion(0, 0).getEndPoint().y;
        for (int i = 0; i <= lastRow; i++) {
            final MutableCell<SpreadSheet> cell = sheet.getCellAt(0, i);
            final String byOO = cell.getTextValue();
            final ODValueType origType = cell.getValueType();
            final Object cellValue = cell.getValue();
            // like OO, we should allow any value without removing the data style
            cell.setValue("string");
            cell.setValue(12.3);
            cell.setValue(new Date());
            cell.setValue(true);
            cell.clearValue();
            if (origType != null)
                cell.setValue(cellValue, origType, false, false);
            assertEquals(byOO, cell.getTextValue()); // <---- ここで落ちてる
            assertEquals(origType, cell.getValueType());
        }

こら! テストコードで for 文書くなって教わらなかったのか! と怒りたいところですがそれはおいといて。

まずは読み込んでるファイルは cellFormat.ods というCalcファイルですね。 中身はこんな感じ。

f:id:naruoga:20210716130933p:plain
cellFormat.odsの内容

で、この1列目を各行ぐるぐる回ってセルの各要素のテキスト文字列を取り出した後、セルに書式指定を無視していろいろ値をセットして(表計算ソフトでは例えば数値書式のセルに文字列を突っ込んだりもできるので)、元の値と書式をセットしなおして結果が変わらないかを見る……って、いうのがテストの意図っぽいですね。

そして、expected が 0,500 で actual が 0.500 なので、元のドキュメントの標準言語はまあ、想像したとおりフランス語なので小数点は , で、だから取り出したセルの値は 0,500 で、cell.setValue() したときには私の動いてる環境の標準ロケールだから日本語で小数点が変わっちゃう……って、ことじゃないかな?

ん? じゃあ、これ、テストの問題じゃなくてプロダクトコードの問題じゃないですかね……。

ともかく、問題になってるのは2行目(セルの表記的には (0,1))なので、そこに条件付きブレークポイントを貼って調べますと、……あれ?

f:id:naruoga:20210716134246p:plain
contentがTextで0,500ってなってる!

テキストじゃん!

あーでも、attributes はこうなのか……。

f:id:naruoga:20210716134601p:plain
attributeでは確かにtype:floatになってる

ふむふむ、ODFの仕様をちゃんと読んでないことがバレバレなのですが、どうやらODFでは

  • セルに文字列で表示するための要素( cell.localElement.content[0]
  • セルに実際に入っている値( cell.attributes

を二重管理してて、前者は少なくともLibreOffice上では使われてない(セルの値と書式から動的に表示結果を作ってる)ってことなのでしょうか……。

で、

cell.setValue(cellValue, origType, false, false);

すると、「ドキュメント」のではなく「セル」の言語が使われて、0.500 になっちゃう、ってこと……みたいです。

……いや、違うかな。違いそう。何が違うかというと

「ドキュメント」のではなく「セル」の言語が使われて

です。というのはなぜかというと、cell.setValue() の実装をおいかけてくと、最終的に NumberStyle.format() ってメソッドに来るんだけど、

    @Override
    public String format(Object o, CellStyle defaultStyle, boolean lenient) {
        final Number n = (Number) o;
        final Namespace numberNS = this.getElement().getNamespace();
        final StringBuilder sb = new StringBuilder();
        @SuppressWarnings("unchecked")
        final List<Element> children = this.getElement().getChildren();
        for (final Element elem : children) {
            if (elem.getNamespace().equals(numberNS)) {
                if (elem.getName().equals("text")) {
                    sb.append(elem.getText());
                } else if (elem.getName().equals("number") || elem.getName().equals("scientific-number")) {
                    sb.append(formatNumberOrScientificNumber(elem, n, defaultStyle));
                } else if (elem.getName().equals("fraction")) {
                    // TODO fractions
                    reportError("Fractions not supported", lenient);
                    sb.append(MutableCell.formatNumber(n, defaultStyle));
                }
            }
        }
        return sb.toString();
    }
}

この中で呼ばれている formatNumberOrScientificNumber(elem, n, defaultStyle)defaultStyle に保持されてるセルのロケール情報見てないっぽい……ですね。 なのでこれはプラダクトコードの問題。

だけど、LibreOfficeというアプリケーションで使うことだけを考えると、テキストは付帯情報であって実際の値やフォーマット情報は適切に扱われているので、ここに不具合があってもすごくシリアスではないし、まあいいかなあという……FIXMEフラグだけつけときますか。

こういうこと考えると下周り(ODFを実際に操作する部分)をごっそりモダンなライブラリ……例えば:

github.com

に差し替えたくなりますね。

ということで乱暴ながら、

  • testFormat() の今落ちてる assert はコメントアウト
  • それ以外の assert は、期待値が即値なのが問題なのでいったん String.format() を利用するように書き換え(将来はセルのロケールに従った動きをしないとだめですが)

としました。

DataStyleTest.testDays() をどうするか

先ほども書いたけど、このテストでは期待値と実際の値が1時間ずれてるというもの。

Expected :27/10/2013 09:30:00
Actual   :27/10/2013 10:30:00

1時間ってことはタイムゾーンというか夏時間(DST)がおかしいんじゃない?と思うんですが、なんかコード見るとまさに夏時間向けの処理がごちゃごちゃと書いてある……。 これを直さなきゃいけないのかー。

と、思いましたが、 このテスト、ちらっと見た感じだと jOpenDocument 自体のテストをしてるように読めないんですよねー。

なので、えいっと無視することにしました。

mavenでdependsしてるのは JUnit4 なので、 @Ignore って書くだけじゃん、と思ってそう書いたら 「これはJUnit3形式のテストだから @Ignore じゃ無視されなくて、メソッド名を test で始まらないように _test ってすればいいよ」とIntellij IDEA君に教えてもらいました。 なんと、まあ。

JUni4 形式に移行することもIntellij IDEAの機能でできるらしいのですが(優秀)、 個人的には将来はJUnit5でもちょっとましなテストを書こうと思ってるんで*2 、 雑に _ をつけて無視する方法にしました。

動かないXMLのバリデーション

ここまで来たら毒皿なので、ここらへんも無視することにしちゃいましょう。

主にバリデーションがテストしたい部分で、これを通さないと落ちるテスト

  • OOXMLTest.testValidation()
  • SheetTest.testCreate()

についてはやはりIgnore(FIXMEつけて)。

それ以外のテストで、テストメソッド内にて SheetTest.assertValid() を呼んでいるテストについては、システムプロパティ jopendocument.test.validationtrue を指定しない限り無視するようにしました。 これも後々は直すつもりですが、まずは先に進みたいので。

ま、そんなわけで、テストは全部バスするようになりました*3

次はCI的なところをやっていきます。

*1:もちろん、スキーマをぶっ壊す可能性があるようなODF操作ライブラリとか怖くて使いたくないので、いずれは直しますよ、もちろん。ただ、優先度をいったん置くってことです。

*2:for文書くな、一つのテストメソッドで延々違うassertを繰り返すんじゃない、的な意味で。

*3:これは品質を満たしているという意味ではないので、まったくよろしくないけど。