VMwareで動作するAlmaLinuxで画面解像度を保存する方法

VMware上で動作しているUbuntuCentOS 7の場合、画面解像度を設定して再起動した場合は、その解像度で起動するのですが、AlmaLinux 8, 9やRocky Linux 8, 9(恐らくCentOS 8, 9 StreamやRHEL8, 9でも同様)では、800x600や1024x768のような低解像で起動してしまいます。

海外のサイトで画面解像度を固定して起動する方法を見つけたのでその手順を紹介します。

参考サイト: https://almalinux.discourse.group/t/almalinux-9-vmware-keeps-resetting-the-screen-resolution-to-800x600/1677

上記サイトでは、画面解像度の設定で使用するスクリプトを特定ユーザーのホームディレクトリ内に置いているのですが、本ブログでは、別のディレクトリに変更しています。

手順:

sudoedit /usr/local/bin/gnome-randr.py

下記サイトを開きCopy file contentsツールチップで表示されるアイコンのクリックでクリップボードにコピーできます。

https://gitlab.com/Oschowa/gnome-randr/-/blob/master/gnome-randr.py

sudoedit /usr/local/bin/resolution.sh

以下の内容にして保存する。

#!/bin/sh
python3 /usr/local/bin/gnome-randr.py --output Virtual-1 --mode 1920x1200

1920x1200 の部分は、指定したい解像度にしてください。
Virtual-1 の部分は、gnome-randr.pyを引数なしで実行した際に表示されるassociated physical monitors:の内容です。VMwareの場合はVirtual-1になりますが、他の仮想OSソフトウェアでは異なると思います。

sudo chmod +x /usr/local/bin/resolution.sh
sudoedit /etc/xdg/autostart/resolution.desktop

以下の内容にして保存する。

[Desktop Entry]
Encoding=UTF-8
Exec=/usr/local/bin/resolution.sh
Name=resolution
Terminal=false
OnlyShowIn=GNOME
Type=Application
StartupNotify=false
X-GNOME-Autostart-Phase=Application

リスタートして確認してみてください。
以上です。

Windows 10 VM (VMware Workstation 15 Pro) をWindows 11にアップグレード

English

仮想マシン(以下VMと略記)のWindows 10 ProにWindows 11 Proをアップグレードインストールしたので、その手順を記載します。 VMware Workstation 15.5.7 Pro(以下VMwareと記載)を使用しています。現時点での最新版は16.2.0なので、1つ前のメジャーバージョンになります。

※自分の使用しているWindowsの言語設定が英語のため、メッセージやキャプチャ画像が英語になっています。適宜日本語に読み替えてください。

インストール用のISOイメージの作成

以下の手順です。

  1. MicorsoftのサイトからISOイメージを作成するツール(Create Windows 11 Installation Media)をダウンロード。
  2. ダウンロードしたMediaCreationToolW11.exeを実行。
  3. Applicable notices and license termsが表示されるので、[Accept]をクリック。
  4. Language/Edtionの選択画面が表示されるので、[Next]をクリック。
  5. ISO file選択して[Next]をクリック。
  6. ファイル名(例: Windows 11.iso)と保存場所を指定して[Save]をクリック。

インストール前の作業

Windows 11へのアップグレードのシステム要件は以下となっています。 (参考サイト: Microsoft) * プロセッサ: 1GHz, 2コア, 64ビット * メモリ: 4GB * ファームウェア: UEFI, セキュア ブート対応 * グラフィックス カード: DirectX 12 (WDDM 2.0) * TPM 2.0

Windows 10 VMが上記要件を満たすように設定します。

ファームウェア: UEFI

ファームウェアタイプがUEFIかどうかは、[VM]-[Settings...]メニュー ⏵ [Options]タブ-[Advanced]-[Firmware type]を確認します。 ここが[BIOS]になっている場合は、[UEFI]に変更する必要がありますが、その前にいったんWindows 10を起動して起動ディスクのパーティションタイプを確認します。 パーティションタイプが[MBR]の場合は、[GPT]に変更する必要があります。変換しないとUEFIに変更後のWindowsの起動に失敗します。

ファームウェアタイプがBIOSの場合

f:id:MasatoKokubo:20211030194117p:plain

起動ディスクのパーティションタイプの確認方法

デスクトップまたはエクスプローラの[This PC/コンテキストメニュー]-[Manage] ⏵ [Storage/Disk Management]

f:id:MasatoKokubo:20211030194136p:plain

[Disk0/コンテキストメニュー]-[Properties] ⏵ [Volumes]タブ (下記はパーティションタイプがMBR)

f:id:MasatoKokubo:20211030194150p:plain

起動ディスクのパーティションタイプを[GPT]に変更するには、コマンドプロンプトを管理者で起動して以下を実行します。

C:\WINDOWS\system32>MBR2GPT /convert /allowFullOS

MBR2GPT will now attempt to convert the default book disk.
If conversion is successful the disk can only be booted in GPT mode.
These changes cannot be undone!

MBR2GPT: Attempting to convert disk 0
MBR2GPT: Retrieving layout of disk
MBR2GPT: Validating layout, disk sector size is: 512 bytes
MBR2GPT: Trying to shrink the OS partition
MBR2GPT: Creating the EFI system partition
MBR2GPT: Installing the new boot files
MBR2GPT: Performing the layout conversion
MBR2GPT: Migrating default boot entry
MBR2GPT: Adding recovery boot entry
MBR2GPT: Fixing drive letter mapping
MBR2GPT: Conversion completed successfully
Call WinReReapir to repair WinRE
MBR2GPT: Failed to update ReAgent.xml, please try to  manually disable and enable WinRE.
MBR2GPT: Before the new system can boot properly you need to switch the firmware to boot to UEFI mode!

Failed to update ReAgent.xmlと表示されていますが、パーティションタイプの変換自体は成功しています。

f:id:MasatoKokubo:20211030194205p:plain

この後Widnowsをシャットダウンして[VM]-[Settings...]で、[Firmware type]を[UEFI]に変更します。 [Secure boot]にチェックを入れる必要はありません。

TPM 2.0

TPM 2.0対応するためには、[VM]-[Settings...]の[Hardware]タブで[Add...]をクリックして [Trusted Platform Module]を追加しますが、その前にVMを暗号化する必要があります。 [Access Control]-[Encrypt...]をクリックして、パスワードを入力して[Encrypt]をクリックします。

f:id:MasatoKokubo:20211030192406p:plain

f:id:MasatoKokubo:20211030193353p:plain

暗号化ができたら[Trusted Platform Module]を追加します。

f:id:MasatoKokubo:20211030193423p:plain

Windows 11のインストール

CD/DVDにWindows 11.isoを割り当ててWindows 10を起動し、DVD Driveのsetup.exeを実行すればWindows 11のインストールが開始されます。

f:id:MasatoKokubo:20211030193450p:plain

f:id:MasatoKokubo:20211030193500p:plain

f:id:MasatoKokubo:20211030193515p:plain

f:id:MasatoKokubo:20211030193524p:plain

f:id:MasatoKokubo:20211030193534p:plain

f:id:MasatoKokubo:20211030193544p:plain

インストール後の作業

ディスクサイズの最小化

Windows 11をインストールすると以前のWindows 10は、C:\Windows.oldに退避されます。Windows 10に戻す事がないのであれば不要なので以下の手順で削除します。

エクスプローラでCドライブのコンテキストメニューから[Properties]を選択し、[Disk Cleanup]をクリック。

f:id:MasatoKokubo:20211030193627p:plain

[Clean up system files]をクリック。

f:id:MasatoKokubo:20211030193640p:plain

すべてにチェックを入れ[OK]をクリック。

f:id:MasatoKokubo:20211030193655p:plain

その後Widnows 11をシャットダウンし、[VM]-[Settings...] ⏵ [Hard Disk]-[Defragment]をクリック。

f:id:MasatoKokubo:20211030193711p:plain

[Compact]をクリック。

f:id:MasatoKokubo:20211030193724p:plain

これで起動ディスクに割り当てられたvmdkファイルのサイズを小さくする事ができます。

TPMハードウエアの削除との暗号化の解除

Windows 11をインストールした後はTPMは不要なので削除します。その後暗号化を解除します。
VMwareVMを開く度にパスワードを入力するのが面倒でなければそのままでもかまいません。

f:id:MasatoKokubo:20211030193739p:plain

f:id:MasatoKokubo:20211030193752p:plain

以上です。

DebugTrace-net チュートリアル

DebugTraceについて

DebugTrace-javaチュートリアルを参照ください。

プロジェクト(C#)の作成

Visual Studio 2019(日本語サイト)を使用するのでインストールします。 Visual Studio 2019の言語が日本語の場合は、以下の内容を適宜日本語に置き換えてください。

  1. Visual Studio 2019を起動後にCreate a new projectをクリックします。(または[File]-[New]-[Project...]を実行)
  2. Create a new Projectダイアログが表示されるので、Class Libraey (.NET Core)を選択してNextをクリックします。
  3. Configure your new projectダイアログが表示されるので、以下を入力してCreateをクリックするとプロジェクトが作成されます。
    Project name: Tutorial
    Location: <任意>
    Solution name: DebugTraceNetTutorial
    ▢ Place solution and project in the same directory
  4. Solution ExplolerでTutorialプロジェクトのDependenciesのコンテキストメニュー(右ボタンでクリック)からManage Nuget Packeges...を選択します。
  5. 生成されたソース(DebugTraceNetTutorial/Tutorial/Class1.cs)を削除し、Tutorialプロジェクト下にsrcフォルダーを作成します。
  6. [File]-[New]-[Project...]を実行します。
  7. Create a new Projectダイアログが表示されるので、MSTest Test Project (.NET Core)を選択してNextをクリックします。
  8. Configure your new projectダイアログが表示されるので、以下を入力してCreateをクリックするとテストプロジェクトが追加されます。
    Project name: TutorialTest
    Location: <変更なし>
    Solution: Add to solution
    Solution Name: <入力不可>
  9. 生成されたソース(DebugTraceNetTutorial/TutorialTest/UnitTest1.cs)を削除し、TutorialTestプロジェクト下にsrcフォルダーを作成します。
  10. TutorialTest/DependenciesのコンテキストメニューからAdd Project Reference...を選択し、Tutorialプロジェクトにチェックを入れ[OK]をクリックします。

f:id:MasatoKokubo:20201123101039p:plain

チュートリアル1

Tutorial/src下に以下のソースファイルを追加します。
パス: DebugTraceNetTutorial\Tutorial\src\Tutorial1.cs

using System.Text;

namespace DebugTraceTutorial {
    public class Tutorial1 {
        public static int MaxLogByteArrayLength = 1024;

        /// <summary>
        /// バイト配列の文字列表現をStringBuilderに追加する。
        /// </summary>
        /// <param name="builder">StringBuilder</param>
        /// <param name="bytes">バイト配列</param>
        /// <returns>引数のbuilder</returns>
        public static StringBuilder AppendBytes(StringBuilder builder, byte[] bytes) {
            builder.Append('[');
            var delimiter = "";
            for (var index = 0; index < bytes.Length; ++index) {
                builder.Append(delimiter);
                if (index >= MaxLogByteArrayLength) {
                    builder.Append("...");
                    break;
                }
                var value = bytes[index];
                var ch = (char)(value / 16 + '0');
                if (ch >= '9') ch += (char)('A' - '9' - 1);
                builder.Append(ch);
                ch = (char)(value % 16 + '0');
                if (ch >= '9') ch += (char)('A' - '9' - 1);
                builder.Append(ch);
                delimiter = ", ";
            }
            builder.Append(']');
            return builder;
        }
    }
}

TutorialTest/src下に以下のソースファイルを追加します。
パス: DebugTraceNetTutorial\TutorialTest\src\Tutorial1Test.cs

using DebugTraceTutorial;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Text;

namespace DebugtraceTutorial {
    [TestClass]
    public class Tutorial1Test {
        /**
         * appendBytesのテスト。
         * @param bytes バイト配列
         * @param expected StringBuilderに追加されると期待する文字列
         */
        [DataTestMethod]
        [DataRow(new byte[] {}, "[]")]
        [DataRow(new byte[] {1}, "[01]")]
        [DataRow(
            new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F},
            "[00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 0A, 0B, 0C, 0D, 0E, 0F]"
        )]
        [DataRow(
            new byte[] {0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF},
            "[F0, F1, F2, F3, F4, F5, F6, F7, F8, F9, FA, FB, FC, FD, FE, FF]"
        )]
        public void TestAppendBytes(byte[] bytes, string expected) {
            // when
            var builder = Tutorial1.AppendBytes(new StringBuilder(), bytes);

            // then
            Assert.AreEqual(expected, builder.ToString());
        }
    }
}

テストを実行すると最初の2つの引数のテストは成功しますが、その後のテストでは失敗します。

テスト結果(最初の失敗):

Assert.AreEqual failed. Expected:<[00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 0A, 0B, 0C, 0D, 0E, 0F]>. Actual:<[00, 01, 02, 03, 04, 05, 06, 07, 08, 0@, 0A, 0B, 0C, 0D, 0E, 0F]>.

08, 0@, 0Aではなく08, 09, 0Aとなるのが正しいです。

DebugTrace-netを使用してみます。

プロジェクトにDebugTrace-netパッケージを以下の手順で追加します。 1. Solution ExplorerTutorial/DependeciesコンテキストメニューからManage NuGet Package...を選択します。 1. Brawseをクリックし、DebugTraceで検索して表示されたリストからDebugTraceを選択してインストールします。

対象のメソッドとテストメソッドにDebugTrace-netのメソッドを呼び出すコードを挿入します。

using System.Text;
using static DebugTrace.CSharp; // TODO: Delete after debugging

namespace DebugTraceTutorial {
    public class Tutorial1 {
        public static int MaxLogByteArrayLength = 1024;

        /// <summary>
        /// バイト配列の文字列表現をStringBuilderに追加する。
        /// </summary>
        /// <param name="builder">StringBuilder</param>
        /// <param name="bytes">バイト配列</param>
        /// <returns>引数のbuilder</returns>
        public static StringBuilder AppendBytes(StringBuilder builder, byte[] bytes) {
            Trace.Enter(); // TODO: Delete after debugging
            Trace.Print("bytes", bytes); // TODO: Delete after debugging
            builder.Append('[');
            var delimiter = "";
            for (var index = 0; index < bytes.Length; ++index) {
                Trace.Print("index", index); // TODO: Delete after debugging
                builder.Append(delimiter);
                if (index >= MaxLogByteArrayLength) {
                    builder.Append("...");
                    break;
                }
                var value = bytes[index];
                Trace.Print("value", value); // TODO: Delete after debugging
                var ch = (char)(value / 16 + '0');
                Trace.Print("1-1 ch", ch); // TODO: Delete after debugging
                if (ch >= '9') ch += (char)('A' - '9' - 1);
                Trace.Print("1-2 ch", ch); // TODO: Delete after debugging
                builder.Append(ch);
                ch = (char)(value % 16 + '0');
                Trace.Print("2-1 ch", ch); // TODO: Delete after debugging
                if (ch >= '9') ch += (char)('A' - '9' - 1);
                Trace.Print("2-2 ch", ch); // TODO: Delete after debugging
                builder.Append(ch);
                delimiter = ", ";
            }
            builder.Append(']');
            Trace.Leave(); // TODO: Delete after debugging
            return builder;
        }
    }
}
using DebugTraceTutorial;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Text;
using static DebugTrace.CSharp; // TODO: Delete after debugging

namespace DebugtraceTutorial {
    [TestClass]
    public class Tutorial1Test {
        /**
         * appendBytesのテスト。
         * @param bytes バイト配列
         * @param expected StringBuilderに追加されると期待する文字列
         */
        [DataTestMethod]
        [DataRow(new byte[] {}, "[]")]
        [DataRow(new byte[] {1}, "[01]")]
        [DataRow(
            new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F},
            "[00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 0A, 0B, 0C, 0D, 0E, 0F]"
        )]
        [DataRow(
            new byte[] {0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF},
            "[F0, F1, F2, F3, F4, F5, F6, F7, F8, F9, FA, FB, FC, FD, FE, FF]"
        )]
        public void TestAppendBytes(byte[] bytes, string expected) {
            Trace.Enter(); // TODO: Delete after debugging
            Trace.Print("bytes", bytes); // TODO: Delete after debugging
            Trace.Print("expected", expected); // TODO: Delete after debugging
            // when
            var builder = Tutorial1.AppendBytes(new StringBuilder(), bytes);

            // then
            Assert.AreEqual(expected, builder.ToString());
            Trace.Leave(); // TODO: Delete after debugging
        }
    }
}

もう一度テストを実行してみます。以下は上記を実行したログの一部です。ログはデフォルトで標準エラー(stderr)に出力されます。
Text ExplorerTest Detail SummaryからOpen additional output for this resultをクリッすると出力内容を確認できます。

2020-11-22 12:03:32.798 [04] Enter DebugtraceTutorial.Tutorial1Test.TestAppendBytes(Byte[] bytes, String expected) (Tutorial1Test.cs:28)
2020-11-22 12:03:32.799 [04] | 
2020-11-22 12:03:32.799 [04] | bytes =
2020-11-22 12:03:32.799 [04] | byte[16] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} (Tutorial1Test.cs:29)
2020-11-22 12:03:32.799 [04] | 
2020-11-22 12:03:32.799 [04] | expected =
2020-11-22 12:03:32.799 [04] | (Length:64)"[00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 0A, 0B, 0C, 0D, 0
2020-11-22 12:03:32.799 [04] | E, 0F]" (Tutorial1Test.cs:30)
2020-11-22 12:03:32.800 [04] | Enter DebugTraceTutorial.Tutorial1.AppendBytes(StringBuilder builder, Byte[] bytes) (Tutorial1.cs:17)
2020-11-22 12:03:32.800 [04] | | 
2020-11-22 12:03:32.800 [04] | | bytes =
2020-11-22 12:03:32.800 [04] | | byte[16] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} (Tutorial1.cs:18)
2020-11-22 12:03:32.800 [04] | | 
2020-11-22 12:03:32.800 [04] | | index = 0 (Tutorial1.cs:22)
2020-11-22 12:03:32.800 [04] | | value = byte 0 (Tutorial1.cs:29)
2020-11-22 12:03:32.801 [04] | | 1-1 ch = '0' (Tutorial1.cs:31)
2020-11-22 12:03:32.801 [04] | | 1-2 ch = '0' (Tutorial1.cs:33)
2020-11-22 12:03:32.801 [04] | | 2-1 ch = '0' (Tutorial1.cs:36)
2020-11-22 12:03:32.802 [04] | | 2-2 ch = '0' (Tutorial1.cs:38)
    ...
2020-11-22 12:03:32.822 [04] | | index = 9 (Tutorial1.cs:22)
2020-11-22 12:03:32.822 [04] | | value = byte 9 (Tutorial1.cs:29)
2020-11-22 12:03:32.822 [04] | | 1-1 ch = '0' (Tutorial1.cs:31)
2020-11-22 12:03:32.824 [04] | | 1-2 ch = '0' (Tutorial1.cs:33)
2020-11-22 12:03:32.825 [04] | | 2-1 ch = '9' (Tutorial1.cs:36)
2020-11-22 12:03:32.826 [04] | | 2-2 ch = '@' (Tutorial1.cs:38)
    ...

バイト配列の9の2桁目が'9'であるはずが'@'に間違って変換されています。 これはif (ch >= '9')の判定が間違っているのでif (ch > '9')に修正します。

修正後もう一度テストを実行すると成功します。ログの一部は以下になります。

2020-11-22 12:04:41.466 [05] | | index = 9 (Tutorial1.cs:22)
2020-11-22 12:04:41.466 [05] | | value = byte 9 (Tutorial1.cs:29)
2020-11-22 12:04:41.466 [05] | | 1-1 ch = '0' (Tutorial1.cs:31)
2020-11-22 12:04:41.468 [05] | | 1-2 ch = '0' (Tutorial1.cs:33)
2020-11-22 12:04:41.468 [05] | | 2-1 ch = '9' (Tutorial1.cs:36)
2020-11-22 12:04:41.468 [05] | | 2-2 ch = '9' (Tutorial1.cs:38)

デバッグの終了後にデバッグ用コードを削除するには、正規表現で検索して空文字列に置換します。改行コードも削除しているのでデバッグ用コードを挿入する前のソースに戻ります。

正規表現検索文字列: ^.+(Debug)?Trace\..*\r?\n

チュートリアル2

次のチュートリアルのソースとテストソースは以下です。
パス: DebugTraceNetTutorial\Tutorial\src\Tutorial2.cs

using System;
using System.Collections.Generic;
using System.Text;

namespace DebugTraceTutorial {
    public class Tutorial2 {
        private static Dictionary<Type, string> shortNames = new Dictionary<Type, string>() {
            {typeof(void)   , "void"   }, {typeof(bool)  , "bool"  },
            {typeof(sbyte)  , "sbyte"  }, {typeof(byte)  , "byte"  },
            {typeof(short)  , "short"  }, {typeof(ushort), "ushort"},
            {typeof(int)    , "int"    }, {typeof(uint)  , "uint"  },
            {typeof(long)   , "long"   }, {typeof(ulong) , "ulong" },
            {typeof(float)  , "float"  }, {typeof(double), "double"},
            {typeof(decimal), "decimal"},
            {typeof(char)   , "char"   }, {typeof(string), "string"},
        };

        /// <summary>
        /// 型名を返します。ただしSystem名前空間の場合は名前空間を取り除いて返します。
        /// </summary>
        /// <param name="type"></param>
        /// <returns>型名</returns>
        public static string GetName(Type type) {
            if (shortNames.ContainsKey(type)) return shortNames[type];
            if (type.IsArray) return GetName(type.GetElementType()) + "[]";
            var typeArguments = type.GenericTypeArguments;
            var backets = ("", "");
            var isValueTuple = type == typeof(ValueTuple);
            if (typeArguments.Length > 0)
                backets = isValueTuple ? ("(", ")") : ("<", ">");
            var builder = new StringBuilder();
            if (!isValueTuple) {
                var typeName = type.Namespace == "System" ? type.Name : type.FullName;
                var backquoteIndex = typeName.IndexOf('`');
                if (backquoteIndex >= 0)
                    typeName = typeName.Substring(0, backquoteIndex);
                builder.Append(typeName);
            }
            builder.Append(backets.Item1);
            var delimiter = "";
            foreach (var typeArgument in typeArguments) {
                builder.Append(delimiter).Append(GetName(typeArgument));
                delimiter = ", ";
            }
            builder.Append(backets.Item2);
            return builder.ToString();
        }
    }
}

パス: DebugTraceNetTutorial\TutorialTest\src\Tutorial2Test.cs

using DebugTraceTutorial;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Data;

namespace DebugtraceTutorial {
    [TestClass]
    public class Tutorial2Test {
        /**
         * GetNameのテスト。
         * @param type 対象の型
         * @param expected 予期する型名
         */
        [DataTestMethod]
        /* 1 */ [DataRow(typeof(string), "string")]
        /* 2 */ [DataRow(typeof(string[]), "string[]")]
        /* 3 */ [DataRow(typeof(string[][]), "string[][]")]
        /* 4 */ [DataRow(typeof(List<string>), "System.Collections.Generic.List<string>")]
        /* 5 */ [DataRow(typeof(List<string>[]), "System.Collections.Generic.List<string>[]")]
        /* 6 */ [DataRow(typeof((int, long)), "(int, long)")]
        /* 7 */ [DataRow(typeof(((byte, short), (int, long))), "((byte, short), (int, long))")]
        public void TestGetName(Type type, string expected) {
            // when
            var typeName = Tutorial2.GetName(type);

            // then
            Assert.AreEqual(expected, typeName);
        }
    }
}

実行すると最初の5つで成功しますが、後の2つ(ValueTuple)では失敗します。

テスト結果(最初の失敗):

Assert.AreEqual failed. Expected:<(int, long)>. Actual:<ValueTuple<int, long>>.

デバッグコードを挿入しますが、メソッドが終了する箇所が3箇所あるため、Trace.Leaveメソッドの呼び出しをGetNameメソッドの最後に挿入だけでは不十分です。 この場合は、以下のようにします。

using System;
using System.Collections.Generic;
using System.Text;
using static DebugTrace.CSharp; // TODO: Delete after debugging

namespace DebugTraceTutorial {
    public class Tutorial2 {
        private static Dictionary<Type, string> shortNames = new Dictionary<Type, string>() {
            {typeof(void)   , "void"   }, {typeof(bool)  , "bool"  },
            {typeof(sbyte)  , "sbyte"  }, {typeof(byte)  , "byte"  },
            {typeof(short)  , "short"  }, {typeof(ushort), "ushort"},
            {typeof(int)    , "int"    }, {typeof(uint)  , "uint"  },
            {typeof(long)   , "long"   }, {typeof(ulong) , "ulong" },
            {typeof(float)  , "float"  }, {typeof(double), "double"},
            {typeof(decimal), "decimal"},
            {typeof(char)   , "char"   }, {typeof(string), "string"},
        };

        /// <summary>
        /// 型名を返します。ただしSystem名前空間の場合は名前空間を取り除いて返します。
        /// </summary>
        /// <param name="type"></param>
        /// <returns>型名</returns>
        public static string GetName(Type type) {
            try {Trace.Enter(); // TODO: Delete after debugging
            Trace.Print("type", type); // TODO: Delete after debugging
            if (shortNames.ContainsKey(type)) return shortNames[type];
            if (type.IsArray) return GetName(type.GetElementType()) + "[]";
            var typeArguments = type.GenericTypeArguments;
            Trace.Print("typeArguments", typeArguments); // TODO: Delete after debugging
            var backets = ("", "");
            var isValueTuple = type == typeof(ValueTuple);
            Trace.Print("typeof(ValueTuple)", typeof(ValueTuple)); // TODO: Delete after debugging
            Trace.Print("isValueTuple", isValueTuple); // TODO: Delete after debugging
            if (typeArguments.Length > 0)
                backets = isValueTuple ? ("(", ")") : ("<", ">");
            Trace.Print("backets", backets); // TODO: Delete after debugging
            var builder = new StringBuilder();
            if (!isValueTuple) {
                var typeName = type.Namespace == "System" ? type.Name : type.FullName;
                var backquoteIndex = typeName.IndexOf('`');
                if (backquoteIndex >= 0)
                    typeName = typeName.Substring(0, backquoteIndex);
                builder.Append(typeName);
            }
            builder.Append(backets.Item1);
            var delimiter = "";
            foreach (var typeArgument in typeArguments) {
                builder.Append(delimiter).Append(GetName(typeArgument));
                delimiter = ", ";
            }
            builder.Append(backets.Item2);
            return builder.ToString();
            } finally {Trace.Leave();} // TODO: Delete after debugging
        }
    }
}

テスト対象のメソッド全体をtry {Trace.Enter();} finally {Trace.Leave();}で囲います。 テストメソッドの方はチュートリアル1と同様にします。
テストを実行すると以下のログ(失敗している部分)が出力されます。

2020-11-22 19:05:52.115 [04] Enter DebugtraceTutorial.Tutorial2Test.TestGetName(Type type, String expected) (Tutorial2Test.cs:27)
2020-11-22 19:05:52.115 [04] | 
2020-11-22 19:05:52.115 [04] | type =
2020-11-22 19:05:52.115 [04] | System.RuntimeType System.ValueTuple`2[System.Int32,System.Int64] (Tutorial2Test.cs:28)
2020-11-22 19:05:52.115 [04] | 
2020-11-22 19:05:52.115 [04] | expected = (Length:11)"(int, long)" (Tutorial2Test.cs:29)
2020-11-22 19:05:52.116 [04] | Enter DebugTraceTutorial.Tutorial2.GetName(Type type) (Tutorial2.cs:27)
2020-11-22 19:05:52.116 [04] | | 
2020-11-22 19:05:52.116 [04] | | type =
2020-11-22 19:05:52.116 [04] | | System.RuntimeType System.ValueTuple`2[System.Int32,System.Int64] (Tutorial2.cs:28)
2020-11-22 19:05:52.118 [04] | | 
2020-11-22 19:05:52.118 [04] | | typeArguments = System.Type[2] {
2020-11-22 19:05:52.118 [04] | |   System.RuntimeType System.Int32, System.RuntimeType System.Int64
2020-11-22 19:05:52.118 [04] | | } (Tutorial2.cs:32)
2020-11-22 19:05:52.119 [04] | | 
2020-11-22 19:05:52.119 [04] | | typeof(ValueTuple) = System.RuntimeType System.ValueTuple (Tutorial2.cs:35)
2020-11-22 19:05:52.119 [04] | | isValueTuple = false (Tutorial2.cs:36)
2020-11-22 19:05:52.120 [04] | | backets = ("<", ">") (Tutorial2.cs:40)
2020-11-22 19:05:52.120 [04] | | Enter DebugTraceTutorial.Tutorial2.GetName(Type type) (Tutorial2.cs:27)
2020-11-22 19:05:52.120 [04] | | | type = System.RuntimeType System.Int32 (Tutorial2.cs:28)
2020-11-22 19:05:52.121 [04] | | Leave DebugTraceTutorial.Tutorial2.GetName(Type type) (Tutorial2.cs:26) duration: 00:00:00.0003259
2020-11-22 19:05:52.121 [04] | | 
2020-11-22 19:05:52.121 [04] | | Enter DebugTraceTutorial.Tutorial2.GetName(Type type) (Tutorial2.cs:27)
2020-11-22 19:05:52.124 [04] | | | type = System.RuntimeType System.Int64 (Tutorial2.cs:28)
2020-11-22 19:05:52.124 [04] | | Leave DebugTraceTutorial.Tutorial2.GetName(Type type) (Tutorial2.cs:26) duration: 00:00:00.0028577
2020-11-22 19:05:52.124 [04] | Leave DebugTraceTutorial.Tutorial2.GetName(Type type) (Tutorial2.cs:26) duration: 00:00:00.0085159

テストの最後のLeaveの出力がないのは、テストが失敗した場合は、途中で例外がスローされてテストメソッドのTrace.Leave()が呼ばれないためです。そこでテストメソッドもtry {}finally{}を使用します。

using DebugTraceTutorial;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Data;
using static DebugTrace.CSharp; // TODO: Delete after debugging

namespace DebugtraceTutorial {
    [TestClass]
    public class Tutorial2Test {
        /**
         * GetNameのテスト。
         * @param type 対象の型
         * @param expected 予期する型名
         */
        [DataTestMethod]
        /* 1 */ [DataRow(typeof(string), "string")]
        /* 2 */ [DataRow(typeof(string[]), "string[]")]
        /* 3 */ [DataRow(typeof(string[][]), "string[][]")]
        /* 4 */ [DataRow(typeof(List<string>), "System.Collections.Generic.List<string>")]
        /* 5 */ [DataRow(typeof(List<string>[]), "System.Collections.Generic.List<string>[]")]
        /* 6 */ [DataRow(typeof((int, long)), "(int, long)")]
        /* 7 */ [DataRow(typeof(((byte, short), (int, long))), "((byte, short), (int, long))")]
        public void TestGetName(Type type, string expected) {
            try {Trace.Enter(); // TODO: Delete after debugging
            Trace.Print("type", type); // TODO: Delete after debugging
            Trace.Print("expected", expected); // TODO: Delete after debugging
            // when
            var typeName = Tutorial2.GetName(type);

            // then
            Assert.AreEqual(expected, typeName);
            } finally {Trace.Leave();} // TODO: Delete after debugging
        }
    }
}

実行してみます。

2020-11-22 19:14:25.609 [04] Enter DebugtraceTutorial.Tutorial2Test.TestGetName(Type type, String expected) (Tutorial2Test.cs:27)
2020-11-22 19:14:25.609 [04] | 
2020-11-22 19:14:25.609 [04] | type =
2020-11-22 19:14:25.609 [04] | System.RuntimeType System.ValueTuple`2[System.Int32,System.Int64] (Tutorial2Test.cs:28)
2020-11-22 19:14:25.609 [04] | 
2020-11-22 19:14:25.609 [04] | expected = (Length:11)"(int, long)" (Tutorial2Test.cs:29)
2020-11-22 19:14:25.610 [04] | Enter DebugTraceTutorial.Tutorial2.GetName(Type type) (Tutorial2.cs:27)
2020-11-22 19:14:25.610 [04] | | 
2020-11-22 19:14:25.610 [04] | | type =
2020-11-22 19:14:25.610 [04] | | System.RuntimeType System.ValueTuple`2[System.Int32,System.Int64] (Tutorial2.cs:28)
2020-11-22 19:14:25.612 [04] | | 
2020-11-22 19:14:25.612 [04] | | typeArguments = System.Type[2] {
2020-11-22 19:14:25.612 [04] | |   System.RuntimeType System.Int32, System.RuntimeType System.Int64
2020-11-22 19:14:25.612 [04] | | } (Tutorial2.cs:32)
2020-11-22 19:14:25.613 [04] | | 
2020-11-22 19:14:25.613 [04] | | typeof(ValueTuple) = System.RuntimeType System.ValueTuple (Tutorial2.cs:35)
2020-11-22 19:14:25.614 [04] | | isValueTuple = false (Tutorial2.cs:36)
2020-11-22 19:14:25.614 [04] | | backets = ("<", ">") (Tutorial2.cs:40)
2020-11-22 19:14:25.614 [04] | | Enter DebugTraceTutorial.Tutorial2.GetName(Type type) (Tutorial2.cs:27)
2020-11-22 19:14:25.615 [04] | | | type = System.RuntimeType System.Int32 (Tutorial2.cs:28)
2020-11-22 19:14:25.615 [04] | | Leave DebugTraceTutorial.Tutorial2.GetName(Type type) (Tutorial2.cs:26) duration: 00:00:00.0003853
2020-11-22 19:14:25.615 [04] | | 
2020-11-22 19:14:25.615 [04] | | Enter DebugTraceTutorial.Tutorial2.GetName(Type type) (Tutorial2.cs:27)
2020-11-22 19:14:25.619 [04] | | | type = System.RuntimeType System.Int64 (Tutorial2.cs:28)
2020-11-22 19:14:25.619 [04] | | Leave DebugTraceTutorial.Tutorial2.GetName(Type type) (Tutorial2.cs:26) duration: 00:00:00.0031286
2020-11-22 19:14:25.619 [04] | Leave DebugTraceTutorial.Tutorial2.GetName(Type type) (Tutorial2.cs:26) duration: 00:00:00.0090726
2020-11-22 19:14:25.622 [04] Leave DebugtraceTutorial.Tutorial2Test.TestGetName(Type type, String expected) (Tutorial2Test.cs:34) duration: 00:00:00.0128997

テストが失敗する原因は、typeValueTupleの場合は、trueになって欲しいvar isValueTuple = type == typeof(ValueTuple);が原因なので、var isValueTuple = type.FullName.StartsWith("System.ValueTuple");に修正します。

デバッグ終了後は、チュートリアル1と同様にデバッグ用コードを削除します。

まとめ

  • チュートリアル1: デバッグコードを挿入する対象のメソッドの終了箇所が1箇所でかつ例外をスローしない場合
    (または例外がスローされた場合にインデントが正しくなくても良い場合)
  • チュートリアル2: デバッグコードを挿入する対象のメソッドの終了箇所が複数か途中で例外をスローする可能性がある場合

以上でチュートリアルは終了です。

DebugTrace-java チュートリアル

DebugTraceについて

デバッグには主に2つの方法があります。

  1. EclipseVisual StudioなどのIDEを使用してブレークポイントで停止させステップ実行する。
  2. プログラムの各所にデバッグ用のログ出力文を挿入する。

1の方法でバグが見つかる場合はそれで良いのですが、複数スレッドで動作するなど複雑なプログラムの場合は、(準備の手間はかかりますが)2の方法の方がバグを見つけやすい場合もあります。

DebugTraceは、2の方法でのデバッグを支援するライブラリです。言語毎にリリースされていて、以下があります。

リフレクションの機能の有無(C++にはない)、メモリ管理方法(ガベッジコレクションを行うかどうか)および命名規約によってそれぞれの言語用のDebugTraceのAPIは異なっていますが、できる事は主に以下の2つです。

  1. メソッド(あるいは関数)の開始時と終了時にログを出力する。またその間のログ出力のインデントを1増やす。
  2. 必要に応じてリフレクションを使用して変数内容をログに出力する。また見易くするため、必要に応じて改行、インデントを行う。

プロジェクト(Java)の作成

AdoptOpenJDKのOpenJDK 11 (LTS)およびEclipse IDE 2020-09 (4.17)を使用するのでインストールします。 JDKはAdoptOpenJDK以外でもかまいません。 Pleiadesを使用して日本語化している場合は、以下の内容を適宜日本語に置き換えてください。

  1. Eclipse起動後に[File]-[New]-[Project...]を実行します。
  2. Wizardの選択ダイアログが表示されるので、Gradle/Gradle Projectを選択して[Next>]をクリックします。
  3. Welcomeダイアログが表示されるので、[Next>]をクリックします。
  4. New Gradle Projectダイアログが表示されるので、Project name:DebugTraceJavaTutorialを入力します。
  5. 入力後[Finish]をクリックするとプロジェクトが作成されます。

生成されたbuild.gradleを以下の内容に置き換えます。

plugins {
    id 'java-library'
}

repositories {
    jcenter()
}

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.7.+'
    testImplementation 'org.junit.jupiter:junit-jupiter-params:5.7.+'

    compileOnly        'org.debugtrace:debugtrace:3.+'
    testImplementation 'org.debugtrace:debugtrace:3.+'
}

sourceCompatibility = '11'
targetCompatibility = '11'

生成されたソース(src/main/java内)をフォルダ毎削除して以下のJavaソースを追加します。
パス: src/main/java/org/debugtrace/tutorial/Tutorial1.java

package org.debugtrace.tutorial;

public class Tutorial1 {
    public static int maxLogByteArrayLength = 1024;

    /**
     * バイト配列の文字列表現をStringBuilderに追加する。
     * @param builder StringBuilder
     * @param bytes バイト配列
     * @return 引数のbuilder
     */
    public static StringBuilder appendBytes(StringBuilder builder, byte[] bytes) {
        builder.append('[');
        var delimiter = "";
        for (var index = 0; index < bytes.length; ++index) {
            builder.append(delimiter);
            if (index >= maxLogByteArrayLength) {
                builder.append("...");
                break;
            }
            var value = bytes[index];
            if (value < 0) value += 256;
            var ch = (char)(value / 16 + '0');
            if (ch > '9') ch += 'A' - '9' - 1;
            builder.append(ch);
            ch = (char)(value % 16 + '0');
            if (ch > '9') ch += 'A' - '9' - 1;
            builder.append(ch);
            delimiter = ", ";
        }
        builder.append(']');

        return builder;
    }
}

生成されたテストソース(src/test/java内)をフォルダ毎削除して以下のJavaソースを追加します。
パス: src/test/java/org/debugtrace/tutorial/Tutorial1Test.java

package org.debugtrace.tutorial;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

public class Tutorial1Test {
    /**
     * appendBytesのテスト。
     * @param bytes バイト配列
     * @param expected StringBuilderに追加されると期待する文字列
     */
    @ParameterizedTest
    @MethodSource("testAppendBytesParams")
    public void testAppendBytes(byte[] bytes, String expected) {
        // when
        var builder = Tutorial1.appendBytes(new StringBuilder(), bytes);

        // then
        assertEquals(expected, builder.toString());
    }

    static Stream<Arguments> testAppendBytesParams() {
        return Stream.of(
            Arguments.of(new byte[] {}, "[]"),
            Arguments.of(new byte[] {1}, "[01]"),
            Arguments.of(
                new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
                "[00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 0A, 0B, 0C, 0D, 0E, 0F]"
            ),
            Arguments.of(
                new byte[] {-16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1},
                "[F0, F1, F2, F3, F4, F5, F6, F7, F8, F9, FA, FB, FC, FD, FE, FF]"
            )
        );
    }
}

チュートリアル1

上記のtestAppendBytesはパラメータ付きのテストメソッドで、testAppendBytesParamsを呼び出して引数を受け取ります。 Arguments.of(...)が一度に渡す引数のセットになっています。

テストを実行すると最初の3つは成功しますが、最後の1つで失敗します。

org.opentest4j.AssertionFailedError: expected: <[F0, F1, F2, F3, F4, F5, F6, F7, F8, F9, FA, FB, FC, FD, FE, FF]> but was: <[/0, 0!, 0", 0#, 0$, 0%, 0&, 0', 0(, 0), 0*, 0+, 0,, 0-, 0., 0/]>

対象のメソッドとテストメソッドにDebugTrace-javaのメソッドを呼び出すコードを挿入してみます。

package org.debugtrace.tutorial;
import org.debugtrace.DebugTrace;

public class Tutorial1 {
    public static int maxLogByteArrayLength = 1024;

    /**
     * バイト配列の文字列表現をStringBuilderに追加する。
     * @param builder StringBuilder
     * @param bytes バイト配列
     * @return 引数のbuilder
     */
    public static StringBuilder appendBytes(StringBuilder builder, byte[] bytes) {
        DebugTrace.enter(); // TODO: Delete after debugging
        DebugTrace.print("bytes", bytes); // TODO: Delete after debugging
        builder.append('[');
        var delimiter = "";
        for (var index = 0; index < bytes.length; ++index) {
            DebugTrace.print("index", index); // TODO: Delete after debugging
            builder.append(delimiter);
            if (index >= maxLogByteArrayLength) {
                builder.append("...");
                break;
            }
            var value = bytes[index];
            DebugTrace.print("1 value", value); // TODO: Delete after debugging
            if (value < 0) value += 256;
            DebugTrace.print("2 value", value); // TODO: Delete after debugging
            var ch = (char)(value / 16 + '0');
            DebugTrace.print("1-1 ch", ch); // TODO: Delete after debugging
            if (ch > '9') ch += 'A' - '9' - 1;
            DebugTrace.print("1-2 ch", ch); // TODO: Delete after debugging
            builder.append(ch);
            ch = (char)(value % 16 + '0');
            DebugTrace.print("2-1 ch", ch); // TODO: Delete after debugging
            if (ch > '9') ch += 'A' - '9' - 1;
            DebugTrace.print("2-2 ch", ch); // TODO: Delete after debugging
            builder.append(ch);
            delimiter = ", ";
        }
        builder.append(']');

        DebugTrace.leave(); // TODO: Delete after debugging
        return builder;
    }
}
package org.debugtrace.tutorial;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.stream.Stream;
import org.debugtrace.DebugTrace;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

public class Tutorial1Test {
    /**
     * appendBytesのテスト。
     * @param bytes バイト配列
     * @param expected StringBuilderに追加されると期待する文字列
     */
    @ParameterizedTest
    @MethodSource("testAppendBytesParams")
    public void testAppendBytes(byte[] bytes, String expected) {
        DebugTrace.enter(); // TODO: Delete after debugging
        // when
        var builder = Tutorial1.appendBytes(new StringBuilder(), bytes);

        // then
        assertEquals(expected, builder.toString());
        DebugTrace.leave(); // TODO: Delete after debugging
    }

    static Stream<Arguments> testAppendBytesParams() {
        return Stream.of(
            Arguments.of(new byte[] {}, "[]"),
            Arguments.of(new byte[] {1}, "[01]"),
            Arguments.of(
                new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
                "[00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 0A, 0B, 0C, 0D, 0E, 0F]"
            ),
            Arguments.of(
                new byte[] {-16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1},
                "[F0, F1, F2, F3, F4, F5, F6, F7, F8, F9, FA, FB, FC, FD, FE, FF]"
            )
        );
    }
}

もう一度テストを実行してみます。以下は上記を実行したログの一部です。ログはデフォルトで標準エラー(stderr)に出力されます。Eclipseで実行した場合は、Consoleに出力されます。

2020-11-02 14:59:58.346+09:00 Enter org.debugtrace.tutorial.Tutorial1Test.testAppendBytes (Tutorial1Test.java:20)
2020-11-02 14:59:58.347+09:00 | Enter org.debugtrace.tutorial.Tutorial1.appendBytes (Tutorial1.java:16)
2020-11-02 14:59:58.347+09:00 | | bytes = (byte[16])[F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF] (Tutorial1.java:17)
2020-11-02 14:59:58.347+09:00 | | index = 0 (Tutorial1.java:21)
2020-11-02 14:59:58.348+09:00 | | 1 value = (byte)-16 (Tutorial1.java:28)
2020-11-02 14:59:58.348+09:00 | | 2 value = (byte)-16 (Tutorial1.java:30)
2020-11-02 14:59:58.348+09:00 | | 1-1 ch = '/' (Tutorial1.java:32)
2020-11-02 14:59:58.348+09:00 | | 1-2 ch = '/' (Tutorial1.java:34)
2020-11-02 14:59:58.349+09:00 | | 2-1 ch = '0' (Tutorial1.java:37)
2020-11-02 14:59:58.349+09:00 | | 2-2 ch = '0' (Tutorial1.java:39)

バイト配列の最初のF0の1桁目のF'/'に間違って変換されています。2 value =の箇所は正値を期待しているのですが、負値のままです。 これはvar value = bytes[index];で、value変数がbyteなのが原因なので、var value = (int)bytes[index];に修正します。

修正後もう一度テストを実行すると成功します。ログの一部は以下になります。

2020-11-02 15:03:51.930+09:00 Enter org.debugtrace.tutorial.Tutorial1Test.testAppendBytes (Tutorial1Test.java:20)
2020-11-02 15:03:51.931+09:00 | Enter org.debugtrace.tutorial.Tutorial1.appendBytes (Tutorial1.java:16)
2020-11-02 15:03:51.931+09:00 | | bytes = (byte[16])[F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF] (Tutorial1.java:17)
2020-11-02 15:03:51.931+09:00 | | index = 0 (Tutorial1.java:21)
2020-11-02 15:03:51.932+09:00 | | 1 value = -16 (Tutorial1.java:29)
2020-11-02 15:03:51.932+09:00 | | 2 value = 240 (Tutorial1.java:31)
2020-11-02 15:03:51.932+09:00 | | 1-1 ch = '?' (Tutorial1.java:33)
2020-11-02 15:03:51.932+09:00 | | 1-2 ch = 'F' (Tutorial1.java:35)
2020-11-02 15:03:51.933+09:00 | | 2-1 ch = '0' (Tutorial1.java:38)
2020-11-02 15:03:51.933+09:00 | | 2-2 ch = '0' (Tutorial1.java:40)

デバッグの終了後にデバッグ用コードを削除するには、正規表現で検索して空文字列に置換します。改行コードも削除しているのでデバッグ用コードを挿入する前のソースに戻ります。

正規表現検索文字列: ^.+DebugTrace.*\r?\n

チュートリアル2

次のチュートリアルのソースとテストソースは以下です。
パス: src/main/java/org/debugtrace/tutorial/Tutorial2.java

package org.debugtrace.tutorial;
import java.util.Objects;

public class Tutorial2 {
    /**
     * クラス名を返します。ただしjava.xxxパッケージの場合はパッケージ名を取り除いて返します。
     * @param clazz クラス
     * @return クラス名
     * @throws NullPointerException clazzがnullの場合
     */
    public static String getName(Class<?> clazz) {
        if (Objects.requireNonNull(clazz, "clazz is null.").isArray())
            return getName(clazz.getComponentType()) + "[]";

        String className = clazz.getName();
        if (className.startsWith("java"))
            return className.substring(className.lastIndexOf('.') + 1);

        return className;
    }
}

パス: src/test/java/org/debugtrace/tutorial/Tutorial2Test.java

package org.debugtrace.tutorial;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.stream.Stream;
import javax.sql.RowSet;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

public class Tutorial2Test {
    /**
     * getNameのテスト。
     * @param clazz クラス
     * @param expected 期待するクラス名
     */
    @ParameterizedTest
    @MethodSource("testGetNameParams")
    public void testGetName(Class<?> clazz, String expected) {
        // when
        var className = Tutorial2.getName(clazz);

        // then
        assertEquals(expected, className);
    }

    static Stream<Arguments> testGetNameParams() {
        return Stream.of(
            Arguments.of(String.class, "String"),
            Arguments.of(String[].class, "String[]"),
            Arguments.of(String[][].class, "String[][]"),
            Arguments.of(RowSet.class, "javax.sql.RowSet"),
            Arguments.of(RowSet[].class, "javax.sql.RowSet[]"),
            Arguments.of(RowSet[][].class, "javax.sql.RowSet[][]")
        );
    }
}

実行すると最初の3つで成功しますが、後の3つでは失敗します。

org.opentest4j.AssertionFailedError: expected: <javax.sql.RowSet> but was: <RowSet>

デバッグコードを挿入しますが、メソッドが終了する箇所が3箇所あるため、DebugTrace.leaveメソッドの呼び出しをgetNameメソッドの最後に挿入だけでは不十分です。 この場合は、以下のようにします。

package org.debugtrace.tutorial;
import java.util.Objects;
import org.debugtrace.DebugTrace;

public class Tutorial2 {
    /**
     * クラス名を返します。ただしjava.xxxパッケージの場合はパッケージ名を取り除いて返します。
     * @param clazz クラス
     * @return クラス名
     * @throws NullPointerException clazzがnullの場合
     */
    public static String getName(Class<?> clazz) {
        try {DebugTrace.enter(); // TODO: Delete after debugging
        DebugTrace.print("clazz", clazz); // TODO: Delete after debugging
        if (Objects.requireNonNull(clazz, "clazz is null.").isArray())
            return getName(clazz.getComponentType()) + "[]";

        String className = clazz.getName();
        DebugTrace.print("className", className); // TODO: Delete after debugging
        DebugTrace.print("className.startsWith(\"java\")", className.startsWith("java")); // TODO: Delete after debugging
        if (className.startsWith("java"))
            return className.substring(className.lastIndexOf('.') + 1);

        return className;
        } finally {DebugTrace.leave();} // TODO: Delete after debugging
    }
}

テスト対象のメソッド全体をtry {DebugTrace.enter();} finally {DebugTrace.leave();}で囲います。 テストメソッドの方はチュートリアル1と同様にします。
テストを実行すると以下のログ(失敗している部分)が出力されます。

2020-11-02 15:28:39.405+09:00 Enter org.debugtrace.tutorial.Tutorial2Test.testGetName (Tutorial2Test.java:22)
2020-11-02 15:28:39.408+09:00 | Enter org.debugtrace.tutorial.Tutorial2.getName (Tutorial2.java:16)
2020-11-02 15:28:39.414+09:00 | | clazz = (Class)interface javax.sql.RowSet (Tutorial2.java:17)
2020-11-02 15:28:39.415+09:00 | | className = (length:16)"javax.sql.RowSet" (Tutorial2.java:22)
2020-11-02 15:28:39.418+09:00 | | className.startsWith("java") = true (Tutorial2.java:23)
2020-11-02 15:28:39.420+09:00 | Leave org.debugtrace.tutorial.Tutorial2.getName (Tutorial2.java:28) duration: 00:00:00.005
2020-11-02 15:28:39.444+09:00 | 
2020-11-02 15:28:39.450+09:00 | Enter org.debugtrace.tutorial.Tutorial2Test.testGetName (Tutorial2Test.java:22)
2020-11-02 15:28:39.450+09:00 | | Enter org.debugtrace.tutorial.Tutorial2.getName (Tutorial2.java:16)
2020-11-02 15:28:39.451+09:00 | | | clazz = (Class)class [Ljavax.sql.RowSet; (Tutorial2.java:17)
2020-11-02 15:28:39.451+09:00 | | | Enter org.debugtrace.tutorial.Tutorial2.getName (Tutorial2.java:16)
2020-11-02 15:28:39.451+09:00 | | | | clazz = (Class)interface javax.sql.RowSet (Tutorial2.java:17)
2020-11-02 15:28:39.451+09:00 | | | | className = (length:16)"javax.sql.RowSet" (Tutorial2.java:22)
2020-11-02 15:28:39.458+09:00 | | | | className.startsWith("java") = true (Tutorial2.java:23)
2020-11-02 15:28:39.461+09:00 | | | Leave org.debugtrace.tutorial.Tutorial2.getName (Tutorial2.java:28) duration: 00:00:00.006
2020-11-02 15:28:39.461+09:00 | | Leave org.debugtrace.tutorial.Tutorial2.getName (Tutorial2.java:28) duration: 00:00:00.010
2020-11-02 15:28:39.474+09:00 | | 
2020-11-02 15:28:39.475+09:00 | | Enter org.debugtrace.tutorial.Tutorial2Test.testGetName (Tutorial2Test.java:22)
2020-11-02 15:28:39.476+09:00 | | | Enter org.debugtrace.tutorial.Tutorial2.getName (Tutorial2.java:16)
2020-11-02 15:28:39.476+09:00 | | | | clazz = (Class)class [[Ljavax.sql.RowSet; (Tutorial2.java:17)
2020-11-02 15:28:39.476+09:00 | | | | Enter org.debugtrace.tutorial.Tutorial2.getName (Tutorial2.java:16)
2020-11-02 15:28:39.477+09:00 | | | | | clazz = (Class)class [Ljavax.sql.RowSet; (Tutorial2.java:17)
2020-11-02 15:28:39.483+09:00 | | | | | Enter org.debugtrace.tutorial.Tutorial2.getName (Tutorial2.java:16)
2020-11-02 15:28:39.484+09:00 | | | | | | clazz = (Class)interface javax.sql.RowSet (Tutorial2.java:17)
2020-11-02 15:28:39.485+09:00 | | | | | | className = (length:16)"javax.sql.RowSet" (Tutorial2.java:22)
2020-11-02 15:28:39.491+09:00 | | | | | | className.startsWith("java") = true (Tutorial2.java:23)
2020-11-02 15:28:39.491+09:00 | | | | | Leave org.debugtrace.tutorial.Tutorial2.getName (Tutorial2.java:28) duration: 00:00:00.007
2020-11-02 15:28:39.492+09:00 | | | | Leave org.debugtrace.tutorial.Tutorial2.getName (Tutorial2.java:28) duration: 00:00:00.014
2020-11-02 15:28:39.492+09:00 | | | Leave org.debugtrace.tutorial.Tutorial2.getName (Tutorial2.java:28) duration: 00:00:00.016

テスト毎にネストが深くなっていきますが、テストが失敗した場合は、途中で例外がスローされてテストメソッドのDebugTrace.leave()が呼ばれないためです。そこでテストメソッドもtry {}finally{}を使用します。

package org.debugtrace.tutorial;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.stream.Stream;
import javax.sql.RowSet;
import org.debugtrace.DebugTrace;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

public class Tutorial2Test {
    /**
     * getNameのテスト。
     * @param clazz クラス
     * @param expected 期待するクラス名
     */
    @ParameterizedTest
    @MethodSource("testGetNameParams")
    public void testGetName(Class<?> clazz, String expected) {
        try {DebugTrace.enter(); // TODO: Delete after debugging
        // when
        var className = Tutorial2.getName(clazz);

        // then
        assertEquals(expected, className);
        } finally {DebugTrace.leave();} // TODO: Delete after debugging
    }

    static Stream<Arguments> testGetNameParams() {
        return Stream.of(
            Arguments.of(String.class, "String"),
            Arguments.of(String[].class, "String[]"),
            Arguments.of(String[][].class, "String[][]"),
            Arguments.of(RowSet.class, "javax.sql.RowSet"),
            Arguments.of(RowSet[].class, "javax.sql.RowSet[]"),
            Arguments.of(RowSet[][].class, "javax.sql.RowSet[][]")
        );
    }
}

実行してみます。

2020-11-02 15:51:39.386+09:00 Enter org.debugtrace.tutorial.Tutorial2Test.testGetName (Tutorial2Test.java:21)
2020-11-02 15:51:39.387+09:00 | Enter org.debugtrace.tutorial.Tutorial2.getName (Tutorial2.java:15)
2020-11-02 15:51:39.387+09:00 | | clazz = (Class)interface javax.sql.RowSet (Tutorial2.java:16)
2020-11-02 15:51:39.388+09:00 | | className = (length:16)"javax.sql.RowSet" (Tutorial2.java:21)
2020-11-02 15:51:39.388+09:00 | | className.startsWith("java") = true (Tutorial2.java:22)
2020-11-02 15:51:39.388+09:00 | Leave org.debugtrace.tutorial.Tutorial2.getName (Tutorial2.java:27) duration: 00:00:00.001
2020-11-02 15:51:39.390+09:00 Leave org.debugtrace.tutorial.Tutorial2Test.testGetName (Tutorial2Test.java:27) duration: 00:00:00.002

テストが失敗する原因は、javax.sql.RowSetの場合は、falseになって欲しいif (className.startsWith("java"))が原因なので、if (className.startsWith("java."))に修正します。

デバッグ終了後は、チュートリアル1と同様にデバッグ用コードを削除します。

まとめ

  • チュートリアル1: デバッグコードを挿入する対象のメソッドの終了箇所が1箇所でかつ例外をスローしない場合
    (または例外がスローされた場合にインデントが正しくなくても良い場合)
  • チュートリアル2: デバッグコードを挿入する対象のメソッドの終了箇所が複数か途中で例外をスローする可能性がある場合

以上でチュートリアルは終了です。

Lightsleep-java 4.0.0をリリースしました

  • Lightsleep-java 4.0.0 - 2020/10/3
    • WITH句対応
    • RECURSIVE SELECT SQL生成対応
    • SELECT句を伴うINSERT SQL生成対応
    • 結合テーブルのサブクエリ対応

f:id:MasatoKokubo:20201005111551p:plain
RECURSIVE SQL作成例

  • Lightsleep-java 3.2.0 - 2019/9/25

  • Lightsleep-java 3.1.1 - 2019/7/18

    • バグ修正
  • Lightsleep-java 3.1.0 - 2019/7/16

    • FROM句サブクエリ対応
    • UNION SELECT SQL生成対応

f:id:MasatoKokubo:20201005111805p:plain
UNION SQL作成例

  • Lightsleep-java 3.0.1 - 2018/12/1
    • バグ修正

README_ja GitHub

リリースノート GitHub

各種DebugTraceを(アップデート)リリースしました

f:id:MasatoKokubo:20201004134422p:plain
実行例 (Java)

f:id:MasatoKokubo:20201004224324p:plain
実行例 (C#)

f:id:MasatoKokubo:20201004211703p:plain
実行例 (JavaScript / Node.js)

f:id:MasatoKokubo:20201004212918p:plain
実行例 (Python)

Lightsleep 3.0.0をリリースしました

DateTime APIのデータ型に対する変換機能を追加しました。 エンティティクラスのフィールド型として以下の型をDate, Time, Timestampの代わりに使用できます。

  • java.time.LocalDate
  • java.time.LocalTime
  • java.time.LocalDateTime
  • java.time.OffsetDateTime
  • java.time.ZonedDateTime
  • java.time.Instant

リリース詳細: GitHub releases