Newer
Older
TIASShot / TIASshot.Tests / IoUtilTests.cs
using System.IO;
using NUnit.Framework;
using OpenCvSharp;

namespace TIASshot.Tests {
    /// <summary>
    /// IoUtil のテスト(CSV 読み書き往復でネイティブ DLL ロードも検証)
    /// </summary>
    [TestFixture]
    public class IoUtilTests {

        private string _csvPath;

        [SetUp]
        public void SetUp() {
            _csvPath = Path.GetTempFileName();
        }

        [TearDown]
        public void TearDown() {
            if (File.Exists(_csvPath)) File.Delete(_csvPath);
        }

        /// <summary>
        /// 2x3 の CV_64FC1 Mat を CSV に保存して再読込したとき、各要素が ±1e-6 以内で一致する
        /// </summary>
        [Test]
        public void SaveAndLoad_2x3Mat_ValuesMatch() {
            // Arrange
            using (var src = CreateMat2x3()) {
                // Act
                IoUtil.SaveMatToCsv(_csvPath, src);
                using (var loaded = IoUtil.LoadMatFromCsv(_csvPath)) {
                    // Assert
                    Assert.IsNotNull(loaded, "ロード結果が null であってはならない");
                    Assert.AreEqual(src.Rows, loaded.Rows, "行数が一致すること");
                    Assert.AreEqual(src.Cols, loaded.Cols, "列数が一致すること");

                    for (int row = 0; row < src.Rows; row++) {
                        for (int col = 0; col < src.Cols; col++) {
                            Assert.AreEqual(
                                src.At<double>(row, col),
                                loaded.At<double>(row, col),
                                1e-6,
                                $"[{row},{col}] の値が ±1e-6 以内で一致すること");
                        }
                    }
                }
            }
        }

        /// <summary>
        /// 1x1 の Mat(スカラー)を CSV に保存して再読込しても値が保たれる
        /// </summary>
        [Test]
        public void SaveAndLoad_1x1Mat_ValuePreserved() {
            // Arrange
            const double Pi = 3.14159265358979;
            using (var src = new Mat(1, 1, MatType.CV_64FC1)) {
                src.At<double>(0, 0) = Pi;

                // Act
                IoUtil.SaveMatToCsv(_csvPath, src);
                using (var loaded = IoUtil.LoadMatFromCsv(_csvPath)) {
                    // Assert
                    Assert.IsNotNull(loaded);
                    Assert.AreEqual(Pi, loaded.At<double>(0, 0), 1e-6);
                }
            }
        }

        /// <summary>
        /// 再現テスト: 小数点にカンマを使うロケール(de-DE)でもピリオド小数の CSV を正しく解釈する
        /// (同梱 tcc_srgb.csv 等はピリオド小数で記述される仕様のため,パースはロケール非依存であること.
        /// カルチャ依存だと "96.32301186" が桁区切り扱いで 9632301186 に化け,例外にならず校正が黙って壊れる)
        /// </summary>
        [Test]
        [NonParallelizable]
        public void LoadMatFromCsv_カンマ小数ロケール_ピリオド小数を正しく解釈する() {
            // Arrange
            File.WriteAllText(_csvPath, "96.32301186,112.325025,186.5234201\r\n");
            var original = System.Threading.Thread.CurrentThread.CurrentCulture;
            try {
                System.Threading.Thread.CurrentThread.CurrentCulture =
                    new System.Globalization.CultureInfo("de-DE");

                // Act
                using (var loaded = IoUtil.LoadMatFromCsv(_csvPath)) {
                    // Assert
                    Assert.IsNotNull(loaded);
                    Assert.AreEqual(96.32301186, loaded.At<double>(0, 0), 1e-6);
                    Assert.AreEqual(112.325025, loaded.At<double>(0, 1), 1e-6);
                    Assert.AreEqual(186.5234201, loaded.At<double>(0, 2), 1e-6);
                }
            } finally {
                System.Threading.Thread.CurrentThread.CurrentCulture = original;
            }
        }

        /// <summary>
        /// 再現テスト: 小数点にカンマを使うロケール(de-DE)でもピリオド小数で CSV を出力する
        /// (カルチャ依存だと小数点がカンマになり,列区切りのカンマと衝突して CSV が壊れる)
        /// </summary>
        [Test]
        [NonParallelizable]
        public void SaveMatToCsv_カンマ小数ロケール_ピリオド小数で出力する() {
            // Arrange
            var original = System.Threading.Thread.CurrentThread.CurrentCulture;
            try {
                System.Threading.Thread.CurrentThread.CurrentCulture =
                    new System.Globalization.CultureInfo("de-DE");
                using (var src = new Mat(1, 2, MatType.CV_64FC1)) {
                    src.At<double>(0, 0) = 1.2345678;
                    src.At<double>(0, 1) = -2.5;

                    // Act
                    IoUtil.SaveMatToCsv(_csvPath, src);

                    // Assert
                    var line = File.ReadAllLines(_csvPath)[0];
                    Assert.AreEqual("1.2345678,-2.5000000", line,
                        "ロケールによらずピリオド小数・カンマ列区切りで出力すること");
                }
            } finally {
                System.Threading.Thread.CurrentThread.CurrentCulture = original;
            }
        }

        /// <summary>
        /// 特性化テスト: ドット小数ロケール(ja-JP)での出力バイト列を固定する
        /// (InvariantCulture 固定化の修正で従来ロケールの出力が 1 バイトも変わらない保護)
        /// </summary>
        [Test]
        [NonParallelizable]
        public void SaveMatToCsv_ドット小数ロケール_出力が従来と同一() {
            // Arrange
            var original = System.Threading.Thread.CurrentThread.CurrentCulture;
            try {
                System.Threading.Thread.CurrentThread.CurrentCulture =
                    new System.Globalization.CultureInfo("ja-JP");
                using (var src = CreateMat2x3()) {
                    // Act
                    IoUtil.SaveMatToCsv(_csvPath, src);

                    // Assert
                    var lines = File.ReadAllLines(_csvPath);
                    Assert.AreEqual("1.2345670,-2.3456780,0.0000010", lines[0]);
                    Assert.AreEqual("100.0000000,-0.5000000,999.9999990", lines[1]);
                }
            } finally {
                System.Threading.Thread.CurrentThread.CurrentCulture = original;
            }
        }

        /// <summary>
        /// 特性化テスト: リポジトリ同梱の tcc_srgb.csv / tcc_xyz.csv が従来どおり読めること
        /// (CameraBase の TCC 参照値読み込み経路の保護.先頭要素の値と行列サイズを固定する)
        /// </summary>
        [Test]
        public void LoadMatFromCsv_同梱tccCsv_従来どおり読める() {
            var repoDir = Path.GetFullPath(Path.Combine(
                TestContext.CurrentContext.TestDirectory,
                "..", "..", "..", "..", "TIASshot"));
            var srgbPath = Path.Combine(repoDir, "tcc_srgb.csv");
            var xyzPath = Path.Combine(repoDir, "tcc_xyz.csv");
            Assert.IsTrue(File.Exists(srgbPath), $"同梱 tcc_srgb.csv が見つからない: {srgbPath}");
            Assert.IsTrue(File.Exists(xyzPath), $"同梱 tcc_xyz.csv が見つからない: {xyzPath}");

            using (var srgb = IoUtil.LoadMatFromCsv(srgbPath))
            using (var xyz = IoUtil.LoadMatFromCsv(xyzPath)) {
                Assert.IsNotNull(srgb, "tcc_srgb.csv が読めること");
                Assert.IsNotNull(xyz, "tcc_xyz.csv が読めること");
                Assert.AreEqual(3, srgb.Cols, "tcc_srgb.csv は 3 列であること");
                Assert.AreEqual(3, xyz.Cols, "tcc_xyz.csv は 3 列であること");
                Assert.AreEqual(96.32301186, srgb.At<double>(0, 0), 1e-6,
                    "tcc_srgb.csv の先頭要素が従来値で読めること");
            }
        }

        /// <summary>
        /// 存在しないファイルをロードしたとき null を返す
        /// </summary>
        [Test]
        public void LoadMatFromCsv_NonExistentFile_ReturnsNull() {
            // Act
            var result = IoUtil.LoadMatFromCsv("does_not_exist_xyz.csv");

            // Assert
            Assert.IsNull(result, "存在しないファイルのロードは null を返すこと");
        }

        // ---------------------------------------------------------------
        // ヘルパー
        // ---------------------------------------------------------------

        /// <summary>
        /// 代表的な 2x3 CV_64FC1 Mat を生成する
        /// </summary>
        private static Mat CreateMat2x3() {
            var m = new Mat(2, 3, MatType.CV_64FC1);
            m.At<double>(0, 0) = 1.234567;
            m.At<double>(0, 1) = -2.345678;
            m.At<double>(0, 2) = 0.000001;
            m.At<double>(1, 0) = 100.0;
            m.At<double>(1, 1) = -0.5;
            m.At<double>(1, 2) = 999.999999;
            return m;
        }
    }
}