ぽらろいどの日記

新しい知見を得たり、得られた知見を記録したり共有したりする場を予定しています。

COEIROINK & VOICEVOXに英語を含むテキストを読ませる with Python on Windows

やりたいこと

冒頭のタイトルに対して、VOICEVOXに英語を読ませる(カタカナ英語) #Python - Qiitaという記事が存在し、これによりカタカナ英語で読ませることが出来る。

なお、記事中では以下の問題が指摘されている。

先のライブラリでは、「アイ ラブ ユー」のように英語の発音を単語ごとに半角スペースで区切るようにしている。 これをVOICEVOXに読ませると、単語間の空白時間が長すぎる。(場所によっては0.5秒ほどの無音が入る)

記事の方法とは別に、英単語の前後で空白が入ること自体を防げる方法があれば、それでも対応可能だと思われる。例えば「この無線LANを」を「この無線」「ラン」「を」と分けずに「この無線ランを」に変更できれば、そもそも無音の時間は生じない。

ここで2つの希望が生じる。

  1. 上記の解決に変更したうえで、txtの文章をVOICEVOXに読ませたい。
  2. COEIROINKでの発話に対応させたい。

自動での改行に対応するようにもしたい。

解決案(VOICEVOX)

そこでまず、1つ目の希望を達成する。①eng_to_kana.pyを変更して期待通りカタカナ英語に変換させる。その後、②これを呼び出して音声合成するスクリプトを作成する。

①カタカナ英語への変換

まずは、eng_to_kana.pyを変更する。生成AIに尋ねつつ、以下の修正となった。

@@ -14,9 +14,6 @@
 reduction=[["It\'s","イッツ"],["I\'m","アイム"],["You\'re","ユーァ"],["He\'s","ヒーィズ"],["She\'s","シーィズ"],["We\'re","ウィーアー"],["They\'re","ゼァー"],["That\'s","ザッツ"],["Who\'s","フーズ"],["Where\'s","フェアーズ"],["I\'d","アイドゥ"],["You\'d","ユードゥ"],["I\'ve","アイブ"],["I\'ll","アイル"],["You\'ll","ユール"],["He\'ll","ヒール"],["She\'ll","シール"],["We\'ll","ウィール"]]
 
 def eng_to_kana(text):
-  # 読みたい記号。他の単語と混ざらないように、前後に半角スペースを挟む
-  text = text.replace("+"," プラス ").replace("+"," プラス ").replace("-"," マイナス ").replace("="," イコール ").replace("="," イコール ")
-
   # No.2、No6みたいに、No.の後に数字が続く場合はノーではなくナンバーと読む
   text = re.sub(r'No\.([0-9])',"ナンバー\\1",text)
   text = re.sub(r'No([0-9])',"ナンバー\\1",text)
@@ -27,21 +24,16 @@
   # this is a pen.のように、aの後に半角スペース、続いてアルファベットの場合、エーではなくアッと呼ぶ
   text = re.sub(r'a ([a-zA-Z])',"アッ \\1",text)
 
-  # 文を区切る文字は消してはダメなので、前後に半角スペースを挟む
-  text = text.replace("."," . ").replace("。"," 。 ").replace("!"," ! ").replace("!"," ! ")
-
-  # アルファベットとアルファベット以外が近接している時、その間に半角スペースを挟む(この後、英単語を単語ごとに区切るための前準備)
-  text_l=list(text)
-  for i in range(len(text))[::-1][:-1]:
-    if re.compile("[a-zA-Z]").search(text_l[i]) and re.compile("[^a-zA-Z]").search(text_l[i-1]): text_l.insert(i," ")
-    elif re.compile("[^a-zA-Z]").search(text_l[i]) and re.compile("[a-zA-Z]").search(text_l[i+-1]): text_l.insert(i," ")
-
-  text = "".join(text_l)
-
-  # 半角スペースや読まなくて良い文字で区切り、各単語の英語をカタカナに変換
-  text_split = re.split('[ \,\*\-\_\=\(\)\[\]\'\"\&\$ ]',text)
-  for i in range(len(text_split)):
-    if str.upper(text_split[i]) in kana_dict:
-      text_split[i] = kana_dict[str.upper(text_split[i])]
+  # アルファベットの文字列を抽出
+  words = re.findall(r"[A-Za-z0-9]+", text)
+  words.sort(key=len, reverse=True)
+
+  # 辞書型の変数kana_dictから読み仮名を取り出し、置換
+  for word in words:
+    # 大文字に変換
+    upper_word = word.upper()
+    if upper_word in kana_dict:
+      # re.sub()を使用して、テキスト内のすべての出現を置換
+      text = re.sub(word, kana_dict[upper_word], text)
 
-  return (" ".join(text_split))
+  return text

変更後ではテキストを分割せず、全体を対象に置換を行う。これにより、元のテキストの構造を変えずに、英語だけをカタカナ英語に変換できる(ということらしい)。

なお「-」をマイナスとして読ませないようにしている。例えばEAP-TTLSはイーエーピーティーティーエルエスと読んでほしいため。

②呼び出して音声合成

つぎに、これを呼び出してテキストをカタカナ英語に変換した後、engineにクエリを投げて音声を再生するスクリプトtalk.pyを作成する。

先記事のsave_voice()では保存してしまうので、これは再生させるように変更する。

# 英語をカタカナに変換するモジュールをインポート
from eng_to_kana import eng_to_kana

# 並行処理を行うためのモジュールをインポート
from concurrent.futures import ThreadPoolExecutor
import json
import requests
import winsound

# スピーカーの詳細を取得する関数
def get_speaker(speaker_name) -> map:
    # スピーカーの情報を取得
    res = requests.get("http://" + voice_server + "/speakers").json()

    # 指定したスピーカーの詳細のみ抽出
    voice_details = list(filter(lambda x: x["name"] == speaker_name, res))

    if len(voice_details) >= 1:
        return voice_details[0]
    else:
        raise Exception("There is no such a speaker.")

# スピーカーの音声IDを取得する関数
def get_voiceid(speaker_name, style_name=None) -> int:
    # スピーカーの詳細を取得
    voice_detail = get_speaker(speaker_name)

    # 指定したスタイルのボイスIDを抽出(未指定ならトップのボイスID)
    if style_name == None:
        voice_styles = voice_detail["styles"]
    else:
        voice_styles = list(
            filter(lambda x: x["name"] == style_name, voice_detail["styles"])
        )

    if len(voice_styles) >= 1:
        return voice_styles[0]["id"]
    else:
        raise Exception("There is no such a style.")

# 音声データを取得する関数
def get_wave_voice(msg, voice_id, custom=[0, 1.1, 1]):
    # メッセージをカタカナに変換
    msg = eng_to_kana(msg)

    # 音声合成クエリを作成
    request = requests.post(
        "http://"
        + voice_server
        + "/audio_query?speaker="
        + str(voice_id)
        + '&text="'
        + msg
        + '"'
    ).json()

    # ピッチ、スピード、イントネーションのスケールを設定
    request["pitchScale"] = custom[0]
    request["speedScale"] = custom[1]
    request["intonationScale"] = custom[2]

    # 音声を合成
    return requests.post(
        "http://" + voice_server + "/synthesis?speaker=" + str(voice_id),
        data=json.dumps(request, ensure_ascii=False).encode("utf-8"),
    )

# 音声サーバーのアドレスを設定
voice_server = "localhost:50021"

def main():
    # スピーカーを指定
    speaker_name = "No.7"
    style_name = None  # デフォルト指定ならNone
    msg_file = "message.txt" # 読むテキストを保存したファイル
    tasklist = []  # 各スレッドのタスクを保持するためのリスト

    # ボイスIDを取得
    voice_id = get_voiceid(speaker_name, style_name)

    with open(msg_file, mode="r", encoding="utf-8") as f:
        # 句点でテキストを分割
        lines = sum([item.split("。") for item in f.readlines()], [])
        with ThreadPoolExecutor() as executor:
            for line in lines:
                # スレッドを使って、各行を音声合成
                tasklist.append(executor.submit(get_wave_voice, line, voice_id))
            for task in tasklist:
                # 合成結果を取得して再生
                wavdata = task.result()
                winsound.PlaySound(wavdata.content, winsound.SND_MEMORY)

main()

解決案(COEIROINK)

つぎに、COEIROINKでの発話に対応させる。

もちろんeng_to_kana.pyは上記の変更と同じで良い。ただしリクエストの形式などが異なるので、呼び出し部talk.pyは変更する必要がある。

# 英語をカタカナに変換するモジュールをインポート
from eng_to_kana import eng_to_kana

# 並行処理を行うためのモジュールをインポート
from concurrent.futures import ThreadPoolExecutor
import json
import requests
import winsound

# スピーカーの詳細を取得する関数
def get_speaker(speaker_name) -> map:
    # スピーカーの詳細を取得
    res = requests.get("http://" + voice_server + "/v1/speakers").json()

    # 指定したスピーカーの詳細のみ抽出
    voice_details = list(filter(lambda x: x["speakerName"] == speaker_name, res))

    if len(voice_details) >= 1:
        return voice_details[0]
    else:
        raise Exception("There is no such a speaker.")

# デフォルトのリクエストを作成する関数
def get_default_request(speaker_name, style_name, custom=[0, 1.2, 1]) -> dict:
    # スピーカーの情報を取得
    speaker_info = get_speaker(speaker_name)

    # 指定したスタイルのスタイルIDを抽出(未指定ならトップのスタイルID)
    voice_styles = list(
        filter(lambda x: x["styleName"] == style_name, speaker_info["styles"])
    )

    if len(voice_styles) > 0:
        style_id = voice_styles[0]["styleId"]
    else:
        style_id = speaker_info["styles"][0]["styleId"]

    # リクエストの内容を作成
    request = {
        "speakerUuid": speaker_info["speakerUuid"],
        "styleId": style_id,
        "text": "",
        "speedScale": custom[1],
        "volumeScale": 1,
        "pitchScale": custom[0],
        "intonationScale": custom[2],
        "prePhonemeLength": 0.1,
        "postPhonemeLength": 0.1,
        "outputSamplingRate": 44100
    }

    return request

# 音声データを取得する関数
def get_wave_voice(msg, request):
    # メッセージをカタカナに変換
    msg = eng_to_kana(msg)

    # メッセージが存在すればリクエストのテキストに設定
    request["text"] = msg if len(msg) > 0 else ""
    
    # 音声を合成
    return requests.post(
        "http://" + voice_server + "/v1/synthesis",
        data=json.dumps(request, ensure_ascii=False).encode("utf-8"),
    )

# 音声サーバーのアドレスを設定
voice_server = "localhost:50032"


def main():
    # スピーカーを指定
    speaker_name = "ディアちゃん"
    style_name = None  # デフォルト指定ならNone
    msg_file = "message.txt" # 読むテキストを保存したファイル
    tasklist = []  # 各スレッドのタスクを保持するためのリスト

    # リクエストのベースを作成
    default_request = get_default_request(speaker_name, style_name)

    with open(msg_file, mode="r", encoding="utf-8") as f:
        # 句点でテキストを分割
        lines = sum([item.split("。") for item in f.readlines()], [])
        # 2スレッドで実施するとエラーっぽいのがengineに出力されるので、一応1
        with ThreadPoolExecutor(max_workers=1) as executor:
            for line in lines:
                # 行に文字がなければスキップ
                if len(''.join(line.split())) == 0:
                    continue
                # 音声データを作成するタスクを追加
                tasklist.append(executor.submit(get_wave_voice, line, default_request))
            for task in tasklist:
                # 音声合成結果を取得し、再生
                wavdata = task.result()
                if wavdata.status_code != 200:
                    continue
                winsound.PlaySound(wavdata.content, winsound.SND_MEMORY)

main()

実際に発話させてみる

あとはrun.exeもしくはengine.exeを起動しておいて、talk.pyを実行すれば良い。

venv環境で実行が面倒であれば、activate.battalk.batなどとしてコピーしておいて、末尾に以下を足せばこのバッチファイルから実行できる(適宜start /MIN /REALTIMEなどとしてよい)。

start python talk.py

コード部分は多いに改善点がありそうだが、とりあえず用は満たしているので良しとする。

【本の感想】『マスタリングTCP/IP 入門編(第6版)』体系的で分かりやすいネットワークの基本

読んだ本

www.ohmsha.co.jp

感想

ネットワークの基本を体系的に分かりやすく学べる本だと感じた。例えばネットワークスペシャリスト試験の学習を始めるときに全く知識がなければ、まずこの本を読んでも良いと思う。

相性が合わなかった書籍

以前『ネットワークはなぜつながるのか 第2版』を読んだことがあったが、こちらはあまり合わなかった。

bookplus.nikkei.com

個人的に、読むタイミングが悪かったと思う。

  • 本の内容
    • 「ブラウザにURLを打ち込んでからの流れ」なので、恐らく整理して理解する向きではなかった
  • デザイン
    • 単純に2007年発売ということもありデザインもやや古い印象があり、苦手意識に繋がってしまった気がする

今回は比較的読みやすかった

その後、こちらの『マスタリングTCP/IP 入門編(第6版)』を読んだ形となる。こちらは整理しながら読むのに適していたし、初学者の自分には向いていた。

  • 本の内容
    • 基礎的なレベルが十分抑えられる
    • 例えばネットワークスペシャリスト試験で言うと、問題さらさら解けるまではいかないけど、解説が大体読めるようになるとおもう
  • デザイン
    • 紙面が広く、余白に自分用のメモを書き込みやすい
    • 章や用語ごとに大きくページを分けていて整理しやすい

学べる知識レベル

ちなみにどの程度の内容が学べるかだが、例えば以下の問題が解ける。

問10 複数のVLANを一つにまとめた単位でスパニングツリーを実現するプロトコルはどれか。

BPDU

GARP

MSTP

RSTP

ネットワークスペシャリスト令和5年春期 午前Ⅱ 問10)

問題冊子・配点割合・解答例・採点講評(2023年度、令和5年度) | 試験情報 | IPA 独立行政法人 情報処理推進機構

もし仮に答えが分からなくても、「VLAN」「スパニングツリー」は概ね知っているので、後は覚え直せば良いという知識レベルである。その意味では、基本が学べると言って良いと思う。

もちろん、そううまくはいかない

ただ単に一読したあとの知識レベルは、現実的には「問題文とか解説の言っている内容は大体分かるけど、そこまでは知らないよ~」ぐらいだと思う。

先の問題も本に軽く説明が入っていたが、そこまで覚えていなかった。

【本の感想】『セキュア・バイ・デザイン』セキュリティと少し親しくなれる本

読んだ本

内容

内容紹介より、目次(一部インデント追加)。

第1部: 導入編

 第1章: なぜ、設計がセキュリティにおいて重要なのか?

 第2章: ちょっと休憩: 『ハムレット』の悲劇

第2部: 基礎編

 第3章: ドメイン駆動設計の中核を成すコンセプト

 第4章: 安全性を確立する実装テクニック

 第5章: ドメイン・プリミティブ(domain primitive)

 第6章: 状態の完全性(integrity)の保証

 第7章: 状態の複雑さの軽減

 第8章: セキュリティを意識したデリバリ・パイプライン

 第9章: 安全性を考えた処理失敗時の対策

 第10章: クラウド的考え方によるメリット

 第11章: ちょっと休憩: 保険料の支払いなしに成立してしまった保険契約

第3部: 応用編

 第12章: レガシー・コードへの適用

 第13章: マイクロサービスでの指針

 第14章: 最後に:セキュリティを忘れるべからず!

まえがきでの説明(p.xii)。

本書の第一の目的となるのは、ソフトウェアの設計とセキュリティがどのように結びつくのかを探求していくことです。(…)本書では、なぜ、特定の設計を選択することがセキュリティにおいて重要となるのか、そして、どのような設計を行うことで安全なソフトウェアを構築できるのか、ということについて学んでいきます。

感想

総じて、以下の点が良かった。

  • セキュリティが、より身近に感じられる
  • 安全な設計への理解を深められる
    • DDDに関連して、ドメインを理解することの重要性、及びそれを通じた凝集度向上
    • 契約による設計、単体テスト、Result型など安全な設計への理解

特に「セキュリティが、より身近に感じられる」については、本書で以下のように述べられているが、今振り返ると、実際に筆者の言うとおりになったように感じる。

多くの開発者はソフトウェアの脆弱性を難解なものとして見ており、(…)そのため、自分自身をセキュリティ対策を行う人物としては捉えておらず、セキュリティ対策はそのことについて詳しい誰か別の人に任せてしまうことが最善だと考える傾向にあります。(…)

(筆者注:設計によって安全性を確立できることが分かれば)セキュリティはその専門家だけが扱うものではなく、ソフトウェアに関わるすべての人の心配事となり、興味の対象となります。(p.26)

自身は開発に関して初学者だと思うが、それでも上記の影響を受けたため、読んで良かったと思った。

詳述

セキュリティが、より身近に感じられる

本書では「設計に対して軽率な判断を下すと、どのようなセキュリティの問題が発生するか」を紹介していて、それによってセキュリティ及びその重要性が身近に感じられた。

例えば、本書では軽率な判断として以下のような例を挙げている。

  • XMLパーサの挙動(pp.30-42)
    • よく考慮せずエンティティの展開を許容することで、Billion Laughs攻撃につながる
  • 汎用的なデータ表現(pp.55-61)
    • 本の購入冊数を単なるintで表現してしまうと、カートにマイナスの冊数の本を含めることが可能になり、購入する本の総額を減らす悪用に繋がりかねない

以上のような例の解説を通じて、普段自分が行っている個々の設計判断が、どんなセキュリティ問題を生んでいるのかを理解できる。

セキュリティの例が決して難解で遠いものではなく、むしろ自分が普段行うコーディングに関わるものとして示されており、自分もセキュリティ問題に関わっているということが理解しやすい。

安全な設計への理解を深められる

加えて、上記の問題に対して、どのように対処するのかも本書では解説している。

例えばXMLパーサについては、XMLそのものの理解もさることながら、以下のような対処も挙げている。

  • 事前に受け取ったデータへの妥当性確認を行い、そこで排除する
  • XMLの解析を他のプロセスと切り離して行うバルクヘッド・パターンを採用する

これらの対処例もまた普段の設計に関するものが多く取り上げられており、「セキュリティ問題に取り組んでいかなければ」と思うようになったあとですぐに実践に移すことが出来ると感じた。

個人的には特に第5章のドメイン・プリミティブと第9章の例外処理がすぐに活用しやすいと思ったが、他にも変数の不変性や妥当性確認、Result型についても触れられており、これらへの理解を深められたのが良かった。

また、新規開発での活用だけでなく、第12章で「レガシー・コードへの適用」、第13章で「マイクロサービスでの指針」について説明しているので、既存のコード改善に適用するアイデアも豊富で、こちらも活用しやすいのではないかと感じた。

個人的な問題

個人的、かつ本書の部分的な問題としては、あまり関わっていない部分については理解が浅くなると感じた。クラウド関連やエンティティ関連はまだ経験が薄いところがあり、そのため実践に向けての理解を進めるのが難しかった。

内容も分厚く、最終章の終わりがp.530なので、全てを読もうとした場合はある程度時間を要することになると思われるが、この点についても上記と関連して半ば流し読みしてしまう点があり、理解が進められていないと感じた。

これらに関しては、後ほど関わりを得たときに都度詳細を読み返して、理解を広げていきたい。

まとめ

自分にとって本書の一番良かったところは、セキュリティへの関心が高められた点だと思う。

セキュリティが難解なものだという印象はあまり変化していないが、それが大きな一枚岩なのではなく幾つかの層から成っていて、自分でも取り組める箇所があるということが分かったのが非常に良かった。

その上で実践に結びつけられる設計パターンの紹介もあるため、すぐに知識を活用しやすいのがありがたかった。

「セキュリティは難しそう…」と思っているひとは、本書を読むことで「自分にもできることがあるかも!」と思えるのではないかと感じた。

MEMO|logback.xmlを使わないで、スクリプト内でファイル出力先の設定をする(slf4j + logback)

概要

logを取るためにslf4j + logbackを用いるが、xmlを利用せずにスクリプトのみでファイル出力先の設定を済ませたい。

以下の解答が見つかったので、メモ。

stackoverflow.com

利用

ついでにimportを補足すると、以下のような形になる(一度動作は確認したけど、最終的な確認してないので微調整は必要かもしれない)。

import ch.qos.logback.classic.encoder.PatternLayoutEncoder
import ch.qos.logback.classic.spi.ILoggingEvent
import ch.qos.logback.core.FileAppender

import org.slf4j.Logger
import org.slf4j.LoggerFactory

LoggerContext lc = LoggerFactory.getILoggerFactory()

PatternLayoutEncoder ple = new PatternLayoutEncoder()
ple.setPattern("%date %level [%thread] %logger{10} [%file:%line] %msg%n")
ple.setContext(lc)
ple.start()

FileAppender<ILoggingEvent> fileAppender = new FileAppender<>()
String logFile = "new.log"
fileAppender.setFile(logFile)
fileAppender.setEncoder(ple)
fileAppender.setContext(lc)
fileAppender.start()

Logger log = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME)
log.addAppender(fileAppender)

log.info("message")

jarファイルを手動セットする場合、以下は少なくとも必要かな?

仕込む場所

ネット上の記事だと以下が参考になりそうだけど、ちょっとした動作記録なら好きに入れれば良いか。

dev.classmethod.jp

qiita.com

初めての設計本に"a philosophy of software design"が良かった

読んだ本

読んだのは以下の第1版

www.amazon.co.jp

内容

ky-yk-d.hatenablog.com

engineers.ntt.com

感想

一言で言うと、まだコーディング経験が不足しているひとが、これからどうやって"良いコーディング"をしていけばいいかを理解するのにちょうどいい本なのかなと思った。

パッと他の設計本・記事を読んだときに、「まだ読むには早いかも……」となる要因として以下があるなと感じていたが、

  • 抽象度が高い表現が中心
    • まだ経験値が少なく、あまり理解できない
    • 凝集度・結合度、DRY原則など
  • 具体的なテクニックが中心
  • 特定の設計手法
    • まずは原則を理解したいので、まだ早い

その点、本著は以下の通りにバランスが良くて、分かりやすかった。

  • 基本的に原則論を展開している
    • より良い設計につながるような考え方のポイント、原則を示している
    • 色々なパターンに対処できる応用力に繋げられそう
  • 原則を色々な角度から示していて、理解しやすい
    • 初めの数章は、ある1つの原則を様々な角度から捉えているような感じがした
    • そのおかげで、抽象的な原則の理解を深めていきやすい
    • 難しい用語も出てこないので、考えやすい

結果的に読み終えてみて、他の本をつまみ読みしたときよりも、"より良いコーディング"について理解を深められた感じがする。

もちろん、追加で色々なテクニックをプラスで学ぶ必要があるとしても、そのテクニックがどうしていいのか・どう使えばいいのかを、ある程度理解できるようになったのではないかと思った。

+α: 個人的に嬉しかった点

個人的には、本書を通じて「流行の技法」について迷いや不安を整理できたのも良かった。例えば、3つ例を挙げると、TDD、コメント、コードの分割がある。

TDD

少しTDDを試した感触で、動作が不安なときのサポートぐらいに使いたいと思っていた一方、「TDDでコーディング当然!」という記事をよく見ていたので、自分の理解にやや不安があった。

この点に関して、著書では以下のように言われていたので、無理して使わなくていいと安心が得られたのが良かった。

Although I am a strong advocate of unit testing, I am not a fan of test-driven development. The problem with test-driven development is that it focuses attention on getting specific feature working, rather than finding the best design. (p.155 / 以下第1版)

コメント

「最善のコードとは、コメントの要らないコードだ」という意見を幾つか見ていて、一方自分の経験ではコメント書いた方が分かりやすい部分があると感じていたので、自分の理解に不安があった。

この点に関して、著書では以下のように言われていたので、コメントを書く安心が得られたのが良かった。加えて、どのように書くべき・書くべきでないか少し整理できたのが良かった。

Some people believe that if code is written well, it is so obvious that no comments are needed. (...) Nonetheless, there is still a significant amount of design information that can't be represented in code.(p.96)

コードの分割

「長すぎる関数は悪い関数」「XX行を超えたら関数を分割しろ」という意見を見ていた一方、実践して分かりにくくなってしまったことがあったので、どう理解すべきか迷っていた。

この点に関して、著書では以下のように言われていて、加えて、実際どのように分割すべきか、考え方や具体例なども提示されていたので、自分の理解を整理できて良かった。

Students in classes are often given rigid criteria, such as "Split up any method longer than 20 lines!" / However, length by itself is rarely a good reason for splitting up a method.(p.70)

以上、初めの頃は色々真に受けすぎるところがあるので、そこで理由や具体例付きで話を整理してもらえたのが嬉しかった。

まとめ

コードの書き方を理解したら、その次は本書にも"I have hypothesized that design skill is what separates great programmers from average ones"(pp.ⅶ-ⅷ)とあるとおり、いわゆる良いコーディングができることが求められるのだと思う。それに関して、本書は良いコーディングの原則を分かりやすく教えてくれるという点で、初めての設計本に適切なのではないかと思った。

まだまだ実践という意味では自分は遠いところにあるので、具体例の豊富な設計本などにも手を伸ばしつつ、レビューなどのタイミングで本書に立ちもどってみて、もっと理解できるようにしていきたい……。

Groovy Scriptをテストする

テスト対象のスクリプトを読み込む方法

1. スクリプトクラスを読み込む

groovy scriptは実行時にクラスとして解釈される。初めからそのようなクラスとして定義していれば、読み込むだけで済む。

クラスとして記述しているので、「スクリプトの一部をSpyする」なども普通にできる。基本的にはこちらで良さそう。

[groovy script]
class groovyscript extends Script {
  def run(){} // should implement

  def func() {
    ...
  }
}

[test]
class ScriptTest extends Specification {
  def "test"() {
    given:
      groovyscript myObject = new groovyscript()
    when:
      def str = myObject.func()
    then:
      str == expected
  }
}

2. ファイルを読み込んで、クラスとして解釈する

Including a groovy script in another groovy - Stack Overflowの方法で、直接スクリプトとして記述したファイルを読み込んで、クラスとして解釈させる。

クラスそのものがあるわけではないので、恐らくSpyとかはできない。変数へのアクセスも面倒だったような。

class ScriptTest extends Specification {
  def "test"() {
    given:
      File sourceFile = new File("groovyscript.groovy")
      Class groovyClass = new GroovyClassLoader(getClass().getClassLoader()).parseClass(sourceFile)
      GroovyObject myObject = (GroovyObject) groovyClass.newInstance()
    when:
      def str = myObject.func()
    then:
      str == expected
  }
}

メモ

mocking static method of inner class

例えば、モック対象が

class Outer {
  class Inner {
    static void a() {}
  }
}

なら、

import Outer
import Outer.Inner

...

given:
  GroovyMock(Outer.Inner, global: true)
when:
  script.func() // static void a()を利用しているgroovy scriptの関数
then:
  1 * Outer.Inner.a()

ただし、configでテストの並列実行をできるようにすると、global: trueにしたモックが期待通り動作しなくなるので、大人しく直列?で実行。

Gradle x Groovy x Jacoco @ローカル環境

以前ローカル環境でCodenarcを利用する例を調べたが、よくよく考えるとJacoco使える方が重要だった。ということで、色々調べつつ試して使えるところまで出来たので、その結果を保存。理解を後回しにしてしまったので、別途Gradle学び直す必要がある……。

plugins {
  id 'groovy'
  id 'jacoco'
}

// jarファイル探索先をローカルに設定
repositories {
  flatDir {
    dirs "/jar/library"
  }
}

// 利用するjarファイル指定
dependencies {
  implementation fileTree(dir: "/groovy/library", include: "*.jar")

  testImplementation "org.spockframework:spock-core:2.2-M1-groovy-4.0"
  testImplementation "net.bytebuddy:byte-buddy:1.12.9"
}

tasks.withType(Test).configureEach {
  useJUnitPlatform()
}

// testタスク実行後にjacocoのレポート作成
test {
  finalizedBy jacocoTestReport
}

jacoco {
  toolVersion = "0.8.8"
}

// jacocoのレポート作成設定
jacocoTestReport {
  // testタスクを要事前実行
  dependsOn test
  // jacocoのjarファイル指定
  jacocoClasspath = fileTree(dir: "/groovy/library", include: "*.jar")

  // レポートから除くファイルの指定
  afterEvaluate {
    classDirectories.setFrom(files(classDirectories.files.collect {
      fileTree(dir: it, exclude: '**/java/lang/**')
    }))
  }

  // 生成するレポートに関する指定
  reports {
    xml.required = false
    csv.required = true
  }
}