MQTTPipeLineTests test 중

This commit is contained in:
김형인
2025-06-10 01:09:36 +09:00
parent ac3645577a
commit 649a359ab4
14 changed files with 886 additions and 70 deletions

View File

@@ -223,7 +223,7 @@ namespace SQLite4Unity3d
/// var db = new SQLiteConnection(Application.persistentDataPath + "/myDatabase.db", true); /// var db = new SQLiteConnection(Application.persistentDataPath + "/myDatabase.db", true);
/// ///
/// // 테이블 생성 /// // 테이블 생성
/// db.CreateTable&lt;Person&gt;(); /// db.CreateTable<Person>();
/// </code> /// </code>
/// </example> /// </example>
public SQLiteConnection(string databasePath, bool storeDateTimeAsTicks = false) public SQLiteConnection(string databasePath, bool storeDateTimeAsTicks = false)
@@ -438,7 +438,7 @@ namespace SQLite4Unity3d
/// <example> /// <example>
/// <code> /// <code>
/// // 제네릭 타입으로 매핑 얻기 /// // 제네릭 타입으로 매핑 얻기
/// var mapping = db.GetMapping&lt;Person&gt;(); /// var mapping = db.GetMapping<Person>();
/// </code> /// </code>
/// </example> /// </example>
public TableMapping GetMapping<T>() public TableMapping GetMapping<T>()
@@ -466,7 +466,7 @@ namespace SQLite4Unity3d
/// <example> /// <example>
/// <code> /// <code>
/// // Person 테이블 삭제 /// // Person 테이블 삭제
/// db.DropTable&lt;Person&gt;(); /// db.DropTable<Person>();
/// </code> /// </code>
/// </example> /// </example>
public int DropTable<T>() public int DropTable<T>()
@@ -490,10 +490,10 @@ namespace SQLite4Unity3d
/// <example> /// <example>
/// <code> /// <code>
/// // Person 클래스를 표현하는 테이블 생성 /// // Person 클래스를 표현하는 테이블 생성
/// db.CreateTable&lt;Person&gt;(); /// db.CreateTable<Person>();
/// ///
/// // 암시적 인덱스 생성 옵션 사용 /// // 암시적 인덱스 생성 옵션 사용
/// db.CreateTable&lt;Person&gt;(CreateFlags.ImplicitIndex); /// db.CreateTable<Person>(CreateFlags.ImplicitIndex);
/// </code> /// </code>
/// </example> /// </example>
public int CreateTable<T>(CreateFlags createFlags = CreateFlags.None) public int CreateTable<T>(CreateFlags createFlags = CreateFlags.None)
@@ -675,7 +675,7 @@ namespace SQLite4Unity3d
/// <summary> /// <summary>
/// 지정된 객체 속성에 대한 인덱스를 생성합니다. /// 지정된 객체 속성에 대한 인덱스를 생성합니다.
/// 예시: CreateIndex&lt;Client&gt;(c => c.Name); /// 예시: CreateIndex<Client>(c => c.Name);
/// </summary> /// </summary>
/// <typeparam name="T">데이터베이스 테이블에 반영할 타입.</typeparam> /// <typeparam name="T">데이터베이스 테이블에 반영할 타입.</typeparam>
/// <param name="property">인덱싱할 속성</param> /// <param name="property">인덱싱할 속성</param>
@@ -683,7 +683,7 @@ namespace SQLite4Unity3d
/// <example> /// <example>
/// <code> /// <code>
/// // 람다 식을 사용하여 속성에 대한 인덱스 생성 /// // 람다 식을 사용하여 속성에 대한 인덱스 생성
/// db.CreateIndex&lt;Person&gt;(p => p.Email, true); /// db.CreateIndex<Person>(p => p.Email, true);
/// </code> /// </code>
/// </example> /// </example>
public void CreateIndex<T>(Expression<Func<T, object>> property, bool unique = false) public void CreateIndex<T>(Expression<Func<T, object>> property, bool unique = false)
@@ -821,7 +821,7 @@ namespace SQLite4Unity3d
/// <code> /// <code>
/// // 파라미터가 있는 명령 생성 /// // 파라미터가 있는 명령 생성
/// var cmd = db.CreateCommand("SELECT * FROM Person WHERE Id = ?", 1); /// var cmd = db.CreateCommand("SELECT * FROM Person WHERE Id = ?", 1);
/// var person = cmd.ExecuteQuery&lt;Person&gt;().FirstOrDefault(); /// var person = cmd.ExecuteQuery<Person>().FirstOrDefault();
/// </code> /// </code>
/// </example> /// </example>
public SQLiteCommand CreateCommand(string cmdText, params object[] ps) public SQLiteCommand CreateCommand(string cmdText, params object[] ps)
@@ -897,8 +897,8 @@ namespace SQLite4Unity3d
/// <example> /// <example>
/// <code> /// <code>
/// // 스칼라 값 가져오기 /// // 스칼라 값 가져오기
/// int count = db.ExecuteScalar&lt;int&gt;("SELECT COUNT(*) FROM Person"); /// int count = db.ExecuteScalar<int>("SELECT COUNT(*) FROM Person");
/// string name = db.ExecuteScalar&lt;string&gt;("SELECT Name FROM Person WHERE Id = ?", 1); /// string name = db.ExecuteScalar<string>("SELECT Name FROM Person WHERE Id = ?", 1);
/// </code> /// </code>
/// </example> /// </example>
public T ExecuteScalar<T>(string query, params object[] args) public T ExecuteScalar<T>(string query, params object[] args)
@@ -944,7 +944,7 @@ namespace SQLite4Unity3d
/// <example> /// <example>
/// <code> /// <code>
/// // 조건에 맞는 모든 Person 객체 조회 /// // 조건에 맞는 모든 Person 객체 조회
/// var adults = db.Query&lt;Person&gt;("SELECT * FROM Person WHERE Age >= ?", 18); /// var adults = db.Query<Person>("SELECT * FROM Person WHERE Age >= ?", 18);
/// foreach (var person in adults) { /// foreach (var person in adults) {
/// Console.WriteLine($"성인: {person.Name}, {person.Age}세"); /// Console.WriteLine($"성인: {person.Name}, {person.Age}세");
/// } /// }
@@ -975,7 +975,7 @@ namespace SQLite4Unity3d
/// <example> /// <example>
/// <code> /// <code>
/// // 대용량 데이터를 지연 로딩으로 처리 /// // 대용량 데이터를 지연 로딩으로 처리
/// var query = db.DeferredQuery&lt;Person&gt;("SELECT * FROM Person"); /// var query = db.DeferredQuery<Person>("SELECT * FROM Person");
/// using (var enumerator = query.GetEnumerator()) { /// using (var enumerator = query.GetEnumerator()) {
/// while (enumerator.MoveNext()) { /// while (enumerator.MoveNext()) {
/// var person = enumerator.Current; /// var person = enumerator.Current;
@@ -1061,7 +1061,7 @@ namespace SQLite4Unity3d
/// <example> /// <example>
/// <code> /// <code>
/// // LINQ를 사용하여 데이터 쿼리하기 /// // LINQ를 사용하여 데이터 쿼리하기
/// var query = db.Table&lt;Person&gt;() /// var query = db.Table<Person>()
/// .Where(p => p.Age > 18) /// .Where(p => p.Age > 18)
/// .OrderBy(p => p.Name); /// .OrderBy(p => p.Name);
/// ///
@@ -1091,7 +1091,7 @@ namespace SQLite4Unity3d
/// <code> /// <code>
/// // 기본 키로 Person 객체 가져오기 /// // 기본 키로 Person 객체 가져오기
/// try { /// try {
/// var person = db.Get&lt;Person&gt;(1); /// var person = db.Get<Person>(1);
/// Console.WriteLine($"이름: {person.Name}"); /// Console.WriteLine($"이름: {person.Name}");
/// } catch (Exception) { /// } catch (Exception) {
/// Console.WriteLine("해당 ID의 Person을 찾을 수 없습니다"); /// Console.WriteLine("해당 ID의 Person을 찾을 수 없습니다");
@@ -1118,7 +1118,7 @@ namespace SQLite4Unity3d
/// <code> /// <code>
/// // 조건식으로 Person 객체 가져오기 /// // 조건식으로 Person 객체 가져오기
/// try { /// try {
/// var person = db.Get&lt;Person&gt;(p => p.Email == "test@example.com"); /// var person = db.Get<Person>(p => p.Email == "test@example.com");
/// Console.WriteLine($"이름: {person.Name}"); /// Console.WriteLine($"이름: {person.Name}");
/// } catch (Exception) { /// } catch (Exception) {
/// Console.WriteLine("조건에 맞는 Person을 찾을 수 없습니다"); /// Console.WriteLine("조건에 맞는 Person을 찾을 수 없습니다");
@@ -1145,7 +1145,7 @@ namespace SQLite4Unity3d
/// <example> /// <example>
/// <code> /// <code>
/// // 기본 키로 Person 객체 찾기 (없으면 null 반환) /// // 기본 키로 Person 객체 찾기 (없으면 null 반환)
/// var person = db.Find&lt;Person&gt;(1); /// var person = db.Find<Person>(1);
/// if (person != null) { /// if (person != null) {
/// Console.WriteLine($"찾았습니다: {person.Name}"); /// Console.WriteLine($"찾았습니다: {person.Name}");
/// } else { /// } else {
@@ -1203,7 +1203,7 @@ namespace SQLite4Unity3d
/// <example> /// <example>
/// <code> /// <code>
/// // 조건식으로 Person 객체 찾기 (없으면 null 반환) /// // 조건식으로 Person 객체 찾기 (없으면 null 반환)
/// var person = db.Find&lt;Person&gt;(p => p.Email == "test@example.com"); /// var person = db.Find<Person>(p => p.Email == "test@example.com");
/// if (person != null) { /// if (person != null) {
/// Console.WriteLine($"찾았습니다: {person.Name}"); /// Console.WriteLine($"찾았습니다: {person.Name}");
/// } else { /// } else {
@@ -1616,10 +1616,10 @@ namespace SQLite4Unity3d
/// // RunInDatabaseLock을 사용한 스레드 안전한 데이터베이스 접근 /// // RunInDatabaseLock을 사용한 스레드 안전한 데이터베이스 접근
/// db.RunInDatabaseLock(() => { /// db.RunInDatabaseLock(() => {
/// // 이 블록 내의 작업은 다른 스레드에서 동일한 데이터베이스에 접근할 수 없습니다. /// // 이 블록 내의 작업은 다른 스레드에서 동일한 데이터베이스에 접근할 수 없습니다.
/// var count = db.ExecuteScalar&lt;int&gt;("SELECT COUNT(*) FROM Person"); /// var count = db.ExecuteScalar<int>("SELECT COUNT(*) FROM Person");
/// ///
/// if (count > 0) { /// if (count > 0) {
/// var people = db.Query&lt;Person&gt;("SELECT * FROM Person"); /// var people = db.Query<Person>("SELECT * FROM Person");
/// foreach (var person in people) { /// foreach (var person in people) {
/// // 안전하게 데이터 처리 /// // 안전하게 데이터 처리
/// ProcessPerson(person); /// ProcessPerson(person);
@@ -1648,7 +1648,7 @@ namespace SQLite4Unity3d
/// <example> /// <example>
/// <code> /// <code>
/// // 객체 목록을 한 번에 삽입 /// // 객체 목록을 한 번에 삽입
/// var people = new List&lt;Person&gt; /// var people = new List<Person>
/// { /// {
/// new Person { Name = "홍길동", Age = 30 }, /// new Person { Name = "홍길동", Age = 30 },
/// new Person { Name = "김철수", Age = 25 }, /// new Person { Name = "김철수", Age = 25 },
@@ -1687,7 +1687,7 @@ namespace SQLite4Unity3d
/// <example> /// <example>
/// <code> /// <code>
/// // 여러 객체 한 번에 삽입 (충돌 무시 옵션 사용) /// // 여러 객체 한 번에 삽입 (충돌 무시 옵션 사용)
/// var people = new List&lt;Person&gt; /// var people = new List<Person>
/// { /// {
/// new Person { Id = 1, Name = "홍길동", Age = 30 }, /// new Person { Id = 1, Name = "홍길동", Age = 30 },
/// new Person { Id = 2, Name = "김철수", Age = 25 }, /// new Person { Id = 2, Name = "김철수", Age = 25 },
@@ -1726,7 +1726,7 @@ namespace SQLite4Unity3d
/// <example> /// <example>
/// <code> /// <code>
/// // 동적 타입으로 여러 객체 한 번에 삽입 /// // 동적 타입으로 여러 객체 한 번에 삽입
/// var people = new List&lt;object&gt; /// var people = new List<object>
/// { /// {
/// new Person { Name = "홍길동", Age = 30 }, /// new Person { Name = "홍길동", Age = 30 },
/// new Person { Name = "김철수", Age = 25 } /// new Person { Name = "김철수", Age = 25 }
@@ -1798,7 +1798,7 @@ namespace SQLite4Unity3d
/// db.InsertOrReplace(new Person { Id = 1, Name = "홍길동(수정)", Age = 31 }); /// db.InsertOrReplace(new Person { Id = 1, Name = "홍길동(수정)", Age = 31 });
/// ///
/// // 값이 변경된 객체를 교체 삽입하는 방식으로 업데이트할 수도 있음 /// // 값이 변경된 객체를 교체 삽입하는 방식으로 업데이트할 수도 있음
/// var person = db.Get&lt;Person&gt;(1); /// var person = db.Get<Person>(1);
/// person.Age += 1; /// person.Age += 1;
/// db.InsertOrReplace(person); /// db.InsertOrReplace(person);
/// </code> /// </code>
@@ -2027,7 +2027,7 @@ namespace SQLite4Unity3d
/// <example> /// <example>
/// <code> /// <code>
/// // 데이터베이스에서 객체 검색 /// // 데이터베이스에서 객체 검색
/// var person = db.Get&lt;Person&gt;(1); /// var person = db.Get<Person>(1);
/// ///
/// // 객체 속성 변경 /// // 객체 속성 변경
/// person.Name = "홍길동(수정됨)"; /// person.Name = "홍길동(수정됨)";
@@ -2127,7 +2127,7 @@ namespace SQLite4Unity3d
/// <example> /// <example>
/// <code> /// <code>
/// // 여러 객체 한 번에 업데이트 /// // 여러 객체 한 번에 업데이트
/// var people = db.Query&lt;Person&gt;("SELECT * FROM Person WHERE Age < 30"); /// var people = db.Query<Person>("SELECT * FROM Person WHERE Age < 30");
/// foreach (var person in people) { /// foreach (var person in people) {
/// person.Age++; /// person.Age++;
/// } /// }
@@ -2161,14 +2161,14 @@ namespace SQLite4Unity3d
/// <example> /// <example>
/// <code> /// <code>
/// // 데이터베이스에서 객체 검색 /// // 데이터베이스에서 객체 검색
/// var person = db.Get&lt;Person&gt;(1); /// var person = db.Get<Person>(1);
/// ///
/// // 객체 삭제 /// // 객체 삭제
/// int rowsAffected = db.Delete(person); /// int rowsAffected = db.Delete(person);
/// Console.WriteLine($"삭제된 행 수: {rowsAffected}"); /// Console.WriteLine($"삭제된 행 수: {rowsAffected}");
/// ///
/// // 또는 직접 ID로 삭제 /// // 또는 직접 ID로 삭제
/// rowsAffected = db.Delete&lt;Person&gt;(2); /// rowsAffected = db.Delete<Person>(2);
/// </code> /// </code>
/// </example> /// </example>
public int Delete(object objectToDelete) public int Delete(object objectToDelete)
@@ -2198,13 +2198,13 @@ namespace SQLite4Unity3d
/// <example> /// <example>
/// <code> /// <code>
/// // 기본 키로 직접 객체 삭제 /// // 기본 키로 직접 객체 삭제
/// int rowsDeleted = db.Delete&lt;Person&gt;(1); /// int rowsDeleted = db.Delete<Person>(1);
/// Console.WriteLine($"ID가 1인 Person 객체 {rowsDeleted}개 삭제됨"); /// Console.WriteLine($"ID가 1인 Person 객체 {rowsDeleted}개 삭제됨");
/// ///
/// // 여러 ID를 차례로 삭제 /// // 여러 ID를 차례로 삭제
/// int[] idsToDelete = { 2, 3, 4 }; /// int[] idsToDelete = { 2, 3, 4 };
/// foreach (int id in idsToDelete) { /// foreach (int id in idsToDelete) {
/// db.Delete&lt;Person&gt;(id); /// db.Delete<Person>(id);
/// } /// }
/// </code> /// </code>
/// </example> /// </example>
@@ -2237,11 +2237,11 @@ namespace SQLite4Unity3d
/// db.BeginTransaction(); /// db.BeginTransaction();
/// try { /// try {
/// // 삭제 전 확인을 위한 카운트 /// // 삭제 전 확인을 위한 카운트
/// int beforeCount = db.ExecuteScalar&lt;int&gt;("SELECT COUNT(*) FROM Person"); /// int beforeCount = db.ExecuteScalar<int>("SELECT COUNT(*) FROM Person");
/// Console.WriteLine($"삭제 전 데이터 수: {beforeCount}"); /// Console.WriteLine($"삭제 전 데이터 수: {beforeCount}");
/// ///
/// // 모든 Person 데이터 삭제 /// // 모든 Person 데이터 삭제
/// int deleted = db.DeleteAll&lt;Person&gt;(); /// int deleted = db.DeleteAll<Person>();
/// ///
/// Console.WriteLine($"삭제된 데이터 수: {deleted}"); /// Console.WriteLine($"삭제된 데이터 수: {deleted}");
/// db.Commit(); /// db.Commit();
@@ -2272,7 +2272,7 @@ namespace SQLite4Unity3d
/// using (var db = new SQLiteConnection("database.db")) /// using (var db = new SQLiteConnection("database.db"))
/// { /// {
/// // 데이터베이스 작업 수행 /// // 데이터베이스 작업 수행
/// var people = db.Query&lt;Person&gt;("SELECT * FROM Person"); /// var people = db.Query<Person>("SELECT * FROM Person");
/// ///
/// // using 블록이 끝나면 자동으로 db.Dispose() 호출됨 /// // using 블록이 끝나면 자동으로 db.Dispose() 호출됨
/// } /// }
@@ -2282,7 +2282,7 @@ namespace SQLite4Unity3d
/// try /// try
/// { /// {
/// // 작업 수행 /// // 작업 수행
/// connection.CreateTable&lt;Person&gt;(); /// connection.CreateTable<Person>();
/// } /// }
/// finally /// finally
/// { /// {
@@ -2312,7 +2312,7 @@ namespace SQLite4Unity3d
/// // 또는 using 문을 사용하여 자동으로 닫기 /// // 또는 using 문을 사용하여 자동으로 닫기
/// using (var db = new SQLiteConnection("database.db")) { /// using (var db = new SQLiteConnection("database.db")) {
/// // 데이터베이스 작업 수행 /// // 데이터베이스 작업 수행
/// var count = db.ExecuteScalar&lt;int&gt;("SELECT COUNT(*) FROM Person"); /// var count = db.ExecuteScalar<int>("SELECT COUNT(*) FROM Person");
/// Console.WriteLine($"Person 테이블의 행 수: {count}"); /// Console.WriteLine($"Person 테이블의 행 수: {count}");
/// ///
/// // using 블록이 끝나면 자동으로 db.Dispose()가 호출되어 연결이 닫힘 /// // using 블록이 끝나면 자동으로 db.Dispose()가 호출되어 연결이 닫힘
@@ -3454,11 +3454,11 @@ namespace SQLite4Unity3d
/// <example> /// <example>
/// <code> /// <code>
/// // 조건식으로 데이터 필터링 /// // 조건식으로 데이터 필터링
/// var adults = db.Table&lt;Person&gt;() /// var adults = db.Table<Person>()
/// .Where(p => p.Age >= 18); /// .Where(p => p.Age >= 18);
/// ///
/// // 여러 조건 체이닝 /// // 여러 조건 체이닝
/// var adultMales = db.Table&lt;Person&gt;() /// var adultMales = db.Table<Person>()
/// .Where(p => p.Age >= 18) /// .Where(p => p.Age >= 18)
/// .Where(p => p.Gender == "남성"); /// .Where(p => p.Gender == "남성");
/// ///
@@ -3519,10 +3519,10 @@ namespace SQLite4Unity3d
/// <example> /// <example>
/// <code> /// <code>
/// // 나이순으로 정렬 /// // 나이순으로 정렬
/// var peopleByAge = db.Table&lt;Person&gt;().OrderBy(p => p.Age); /// var peopleByAge = db.Table<Person>().OrderBy(p => p.Age);
/// ///
/// // 이름순으로 정렬 /// // 이름순으로 정렬
/// var peopleByName = db.Table&lt;Person&gt;().OrderBy(p => p.Name); /// var peopleByName = db.Table<Person>().OrderBy(p => p.Name);
/// ///
/// foreach (var person in peopleByAge) { /// foreach (var person in peopleByAge) {
/// Console.WriteLine($"{person.Name}, {person.Age}세"); /// Console.WriteLine($"{person.Name}, {person.Age}세");
@@ -3543,10 +3543,10 @@ namespace SQLite4Unity3d
/// <example> /// <example>
/// <code> /// <code>
/// // 나이 내림차순으로 정렬 /// // 나이 내림차순으로 정렬
/// var olderFirst = db.Table&lt;Person&gt;().OrderByDescending(p => p.Age); /// var olderFirst = db.Table<Person>().OrderByDescending(p => p.Age);
/// ///
/// // 이름 내림차순으로 정렬 /// // 이름 내림차순으로 정렬
/// var reverseAlpha = db.Table&lt;Person&gt;().OrderByDescending(p => p.Name); /// var reverseAlpha = db.Table<Person>().OrderByDescending(p => p.Name);
/// ///
/// foreach (var person in olderFirst) { /// foreach (var person in olderFirst) {
/// Console.WriteLine($"{person.Name}, {person.Age}세"); /// Console.WriteLine($"{person.Name}, {person.Age}세");

View File

@@ -77,7 +77,7 @@ namespace UVC.Data
/// var mapper = new DataMapper(maskJson); /// var mapper = new DataMapper(maskJson);
/// DataObject result = mapper.Map(sourceJson); /// DataObject result = mapper.Map(sourceJson);
/// Debug.Log(result["name"].ToString()); // "김철수" /// Debug.Log(result["name"].ToString()); // "김철수"
/// Debug.Log(result["age"].ToObject&lt;int&gt;()); // 30 /// Debug.Log(result["age"].ToObject<int>()); // 30
/// </code> /// </code>
/// </example> /// </example>
public DataObject Mapping(JObject source) public DataObject Mapping(JObject source)

View File

@@ -30,7 +30,7 @@ namespace UVC.Data
/// mask.ObjectName = "employees"; // 데이터 객체의 이름 지정 /// mask.ObjectName = "employees"; // 데이터 객체의 이름 지정
/// ///
/// // 필드 이름 변환 규칙 설정 /// // 필드 이름 변환 규칙 설정
/// mask.NamesForReplace = new Dictionary&lt;string, string&gt; /// mask.NamesForReplace = new Dictionary<string, string>
/// { /// {
/// { "full_name", "name" }, // JSON의 full_name을 name으로 변환 /// { "full_name", "name" }, // JSON의 full_name을 name으로 변환
/// { "employee_age", "age" } // JSON의 employee_age를 age로 변환 /// { "employee_age", "age" } // JSON의 employee_age를 age로 변환
@@ -87,7 +87,7 @@ namespace UVC.Data
/// <example> /// <example>
/// <code> /// <code>
/// var mask = new DataMask(); /// var mask = new DataMask();
/// mask.NamesForReplace = new Dictionary&lt;string, string&gt; /// mask.NamesForReplace = new Dictionary<string, string>
/// { /// {
/// { "first_name", "firstName" }, /// { "first_name", "firstName" },
/// { "last_name", "lastName" }, /// { "last_name", "lastName" },

View File

@@ -24,7 +24,7 @@ namespace UVC.Data
/// mask.ObjectName = "users"; /// mask.ObjectName = "users";
/// ///
/// // 필드 이름 변환 설정 /// // 필드 이름 변환 설정
/// mask.NamesForReplace = new Dictionary&lt;string, string&gt; /// mask.NamesForReplace = new Dictionary<string, string>
/// { /// {
/// { "userName", "name" }, /// { "userName", "name" },
/// { "userEmail", "email" } /// { "userEmail", "email" }

View File

@@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq;
using SampleProject.Config; using SampleProject.Config;
using System.Collections.Generic; using System.Collections.Generic;
using UVC.network; using UVC.network;
using UVC.Tests;
namespace UVC.Data namespace UVC.Data
{ {
@@ -17,6 +18,16 @@ namespace UVC.Data
/// </remarks> /// </remarks>
public class MQTTPipeLine public class MQTTPipeLine
{ {
/// <summary>
/// 테스트를 위한 목업 모드 활성화 여부를 설정하거나 가져옵니다.
/// </summary>
/// <remarks>
/// true로 설정하면 실제 MQTT 요청 대신 MQTTPipeLine를 사용합니다.
/// 테스트 환경에서 외부 의존성 없이 MQTT 통신을 시뮬레이션할 때 유용합니다.
/// </remarks>
public bool UseMockup { get; internal set; } = false;
/// <summary> /// <summary>
/// MQTT 브로커의 도메인 주소 /// MQTT 브로커의 도메인 주소
/// </summary> /// </summary>
@@ -37,6 +48,8 @@ namespace UVC.Data
/// </summary> /// </summary>
private MQTTService mqtt; private MQTTService mqtt;
private MockMQTTService? mockupMQTT;
/// <summary> /// <summary>
/// MQTTPipeLine 인스턴스를 생성합니다. /// MQTTPipeLine 인스턴스를 생성합니다.
/// </summary> /// </summary>
@@ -86,11 +99,24 @@ namespace UVC.Data
/// </summary> /// </summary>
public void Execute() public void Execute()
{ {
foreach (var topic in infoList.Keys) if (!UseMockup)
{ {
mqtt.AddTopicHandler(topic, OnTopicMessage); foreach (var topic in infoList.Keys)
{
mqtt.AddTopicHandler(topic, OnTopicMessage);
}
mqtt.Connect();
}
else
{
// Mockup 모드인 경우 MockMQTTService를 사용하여 테스트 환경을 설정합니다.
mockupMQTT = new MockMQTTService();
foreach (var topic in infoList.Keys)
{
mockupMQTT.AddTopicHandler(topic, OnTopicMessage);
}
mockupMQTT.Connect();
} }
mqtt.Connect();
} }
/// <summary> /// <summary>
@@ -141,11 +167,19 @@ namespace UVC.Data
/// </summary> /// </summary>
public void Stop() public void Stop()
{ {
foreach (var topic in infoList.Keys) if (!UseMockup)
{ {
mqtt.RemoveTopicHandler(topic, OnTopicMessage); foreach (var topic in infoList.Keys)
{
mqtt.RemoveTopicHandler(topic, OnTopicMessage);
}
mqtt.Disconnect();
}
else
{
// Mockup 모드인 경우 MockMQTTService를 사용하여 연결을 종료합니다.
mockupMQTT?.Disconnect();
} }
mqtt.Disconnect();
} }
/// <summary> /// <summary>
@@ -155,7 +189,8 @@ namespace UVC.Data
/// <see cref="Dispose"/>를 호출한 후에는 해당 인스턴스를 더 이상 사용할 수 없습니다.</remarks> /// <see cref="Dispose"/>를 호출한 후에는 해당 인스턴스를 더 이상 사용할 수 없습니다.</remarks>
public void Dispose() public void Dispose()
{ {
mqtt.Disconnect(); if (!UseMockup) mqtt.Disconnect();
else mockupMQTT?.Disconnect();
infoList.Clear(); infoList.Clear();
} }

View File

@@ -20,11 +20,11 @@ namespace UVC.Extension
/// <example> /// <example>
/// <code> /// <code>
/// // 게임 오브젝트의 RectTransform에 여백 적용 /// // 게임 오브젝트의 RectTransform에 여백 적용
/// RectTransform panelRect = panel.GetComponent&lt;RectTransform&gt;(); /// RectTransform panelRect = panel.GetComponent<RectTransform>();
/// panelRect.SetRectMargin(10f, 10f, 10f, 10f); // 사방 10픽셀 여백 설정 /// panelRect.SetRectMargin(10f, 10f, 10f, 10f); // 사방 10픽셀 여백 설정
/// ///
/// // UI 요소를 부모 컨테이너에 맞추되 여백 주기 /// // UI 요소를 부모 컨테이너에 맞추되 여백 주기
/// RectTransform childRect = childObject.GetComponent&lt;RectTransform&gt;(); /// RectTransform childRect = childObject.GetComponent<RectTransform>();
/// childRect.SetRectMargin(5f, 5f, 20f, 5f); // 상단에 더 큰 여백 설정 /// childRect.SetRectMargin(5f, 5f, 20f, 5f); // 상단에 더 큰 여백 설정
/// </code> /// </code>
/// </example> /// </example>
@@ -44,7 +44,7 @@ namespace UVC.Extension
/// <example> /// <example>
/// <code> /// <code>
/// // 버튼의 너비를 200으로 설정 /// // 버튼의 너비를 200으로 설정
/// RectTransform buttonRect = button.GetComponent&lt;RectTransform&gt;(); /// RectTransform buttonRect = button.GetComponent<RectTransform>();
/// buttonRect.SetWidth(200f); /// buttonRect.SetWidth(200f);
/// </code> /// </code>
/// </example> /// </example>
@@ -61,7 +61,7 @@ namespace UVC.Extension
/// <example> /// <example>
/// <code> /// <code>
/// // 패널의 높이를 150으로 설정 /// // 패널의 높이를 150으로 설정
/// RectTransform panelRect = panel.GetComponent&lt;RectTransform&gt;(); /// RectTransform panelRect = panel.GetComponent<RectTransform>();
/// panelRect.SetHeight(150f); /// panelRect.SetHeight(150f);
/// </code> /// </code>
/// </example> /// </example>
@@ -78,7 +78,7 @@ namespace UVC.Extension
/// <example> /// <example>
/// <code> /// <code>
/// // 이미지의 크기를 100x100으로 설정 /// // 이미지의 크기를 100x100으로 설정
/// RectTransform imageRect = image.GetComponent&lt;RectTransform&gt;(); /// RectTransform imageRect = image.GetComponent<RectTransform>();
/// imageRect.SetSize(new Vector2(100f, 100f)); /// imageRect.SetSize(new Vector2(100f, 100f));
/// </code> /// </code>
/// </example> /// </example>
@@ -95,7 +95,7 @@ namespace UVC.Extension
/// <example> /// <example>
/// <code> /// <code>
/// // 버튼을 화면 중앙에 위치시키기 /// // 버튼을 화면 중앙에 위치시키기
/// RectTransform buttonRect = button.GetComponent&lt;RectTransform&gt;(); /// RectTransform buttonRect = button.GetComponent<RectTransform>();
/// buttonRect.SetAnchorsToCenter(); /// buttonRect.SetAnchorsToCenter();
/// buttonRect.anchoredPosition = Vector2.zero; // 중앙 위치에 배치 /// buttonRect.anchoredPosition = Vector2.zero; // 중앙 위치에 배치
/// </code> /// </code>
@@ -114,7 +114,7 @@ namespace UVC.Extension
/// <example> /// <example>
/// <code> /// <code>
/// // UI 요소를 왼쪽 상단에 위치시키기 /// // UI 요소를 왼쪽 상단에 위치시키기
/// RectTransform elementRect = element.GetComponent&lt;RectTransform&gt;(); /// RectTransform elementRect = element.GetComponent<RectTransform>();
/// elementRect.SetAnchorsToTopLeft(); /// elementRect.SetAnchorsToTopLeft();
/// elementRect.anchoredPosition = new Vector2(10f, -10f); // 약간의 여백 추가 /// elementRect.anchoredPosition = new Vector2(10f, -10f); // 약간의 여백 추가
/// </code> /// </code>
@@ -133,7 +133,7 @@ namespace UVC.Extension
/// <example> /// <example>
/// <code> /// <code>
/// // 배경 이미지를 패널 전체에 채우기 /// // 배경 이미지를 패널 전체에 채우기
/// RectTransform backgroundRect = backgroundImage.GetComponent&lt;RectTransform&gt;(); /// RectTransform backgroundRect = backgroundImage.GetComponent<RectTransform>();
/// backgroundRect.StretchToParentEdges(); /// backgroundRect.StretchToParentEdges();
/// </code> /// </code>
/// </example> /// </example>
@@ -150,7 +150,7 @@ namespace UVC.Extension
/// <example> /// <example>
/// <code> /// <code>
/// // 요소를 부모의 오른쪽 상단에 배치 /// // 요소를 부모의 오른쪽 상단에 배치
/// RectTransform elementRect = element.GetComponent&lt;RectTransform&gt;(); /// RectTransform elementRect = element.GetComponent<RectTransform>();
/// elementRect.SetNormalizedPosition(new Vector2(0.95f, 0.95f)); /// elementRect.SetNormalizedPosition(new Vector2(0.95f, 0.95f));
/// </code> /// </code>
/// </example> /// </example>
@@ -177,7 +177,7 @@ namespace UVC.Extension
/// <example> /// <example>
/// <code> /// <code>
/// // UI 요소가 특정 월드 좌표를 포함하는지 확인 /// // UI 요소가 특정 월드 좌표를 포함하는지 확인
/// RectTransform elementRect = element.GetComponent&lt;RectTransform&gt;(); /// RectTransform elementRect = element.GetComponent<RectTransform>();
/// Rect worldRect = elementRect.GetWorldRect(); /// Rect worldRect = elementRect.GetWorldRect();
/// Vector3 worldPos = Camera.main.ScreenToWorldPoint(Input.mousePosition); /// Vector3 worldPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
/// bool isOverUI = worldRect.Contains(new Vector2(worldPos.x, worldPos.y)); /// bool isOverUI = worldRect.Contains(new Vector2(worldPos.x, worldPos.y));

View File

@@ -116,7 +116,7 @@ namespace UVC.Extension
/// <code> /// <code>
/// // 바이트 배열에서 객체 복원 /// // 바이트 배열에서 객체 복원
/// byte[] savedData = GetSavedDataFromSomewhere(); /// byte[] savedData = GetSavedDataFromSomewhere();
/// Person restoredPerson = savedData.DeserializeFromBytes&lt;Person&gt;(); /// Person restoredPerson = savedData.DeserializeFromBytes<Person>();
/// ///
/// if (restoredPerson != null) /// if (restoredPerson != null)
/// { /// {
@@ -184,7 +184,7 @@ namespace UVC.Extension
/// <returns>역직렬화된 객체 또는 실패 시 null</returns> /// <returns>역직렬화된 객체 또는 실패 시 null</returns>
/// <example> /// <example>
/// <code> /// <code>
/// GameSettings settings = DeserializeFromFile&lt;GameSettings&gt;("Assets/settings.dat"); /// GameSettings settings = DeserializeFromFile<GameSettings>("Assets/settings.dat");
/// if (settings != null) /// if (settings != null)
/// { /// {
/// ApplySettings(settings.Volume, settings.Difficulty); /// ApplySettings(settings.Volume, settings.Difficulty);

View File

@@ -60,12 +60,12 @@ namespace UVC.Json
/// <code> /// <code>
/// // 단일 객체 역직렬화 /// // 단일 객체 역직렬화
/// string userJson = "{\"Id\":1,\"Name\":\"홍길동\"}"; /// string userJson = "{\"Id\":1,\"Name\":\"홍길동\"}";
/// User user = JsonHelper.FromJson&lt;User&gt;(userJson); /// User user = JsonHelper.FromJson<User>(userJson);
/// Debug.Log($"사용자 정보: {user.Id}, {user.Name}"); /// Debug.Log($"사용자 정보: {user.Id}, {user.Name}");
/// ///
/// // 컬렉션 역직렬화 /// // 컬렉션 역직렬화
/// string arrayJson = "[{\"Id\":1,\"Name\":\"홍길동\"},{\"Id\":2,\"Name\":\"김철수\"}]"; /// string arrayJson = "[{\"Id\":1,\"Name\":\"홍길동\"},{\"Id\":2,\"Name\":\"김철수\"}]";
/// List&lt;User&gt; users = JsonHelper.FromJson&lt;List&lt;User&gt;&gt;(arrayJson); /// List<User> users = JsonHelper.FromJson<List<User>>(arrayJson);
/// foreach(var u in users) { /// foreach(var u in users) {
/// Debug.Log($"사용자 정보: {u.Id}, {u.Name}"); /// Debug.Log($"사용자 정보: {u.Id}, {u.Name}");
/// } /// }

View File

@@ -35,7 +35,6 @@ namespace UVC.network
/// <value>클라이언트가 초기화되고 브로커에 연결된 경우 <c>true</c>를 반환합니다.</value> /// <value>클라이언트가 초기화되고 브로커에 연결된 경우 <c>true</c>를 반환합니다.</value>
public bool IsConnected => client != null && client.State == ClientStates.Connected; public bool IsConnected => client != null && client.State == ClientStates.Connected;
private Action<string, string> onMessageReceived;
/// <summary> /// <summary>
/// MQTTService 인스턴스를 생성합니다. /// MQTTService 인스턴스를 생성합니다.
@@ -126,7 +125,7 @@ namespace UVC.network
/// <example> /// <example>
/// <code> /// <code>
/// // 핸들러 정의 /// // 핸들러 정의
/// Action&lt;string, string&gt; temperatureHandler = (topic, message) => { /// Action<string, string> temperatureHandler = (topic, message) => {
/// Debug.Log($"온도 데이터 수신: {message}"); /// Debug.Log($"온도 데이터 수신: {message}");
/// }; /// };
/// ///
@@ -324,9 +323,11 @@ namespace UVC.network
string payload = Encoding.UTF8.GetString(message.Payload.Data, message.Payload.Offset, message.Payload.Count); string payload = Encoding.UTF8.GetString(message.Payload.Data, message.Payload.Offset, message.Payload.Count);
Debug.Log($"MQTT OnTopic {topic.Filter.OriginalFilter} => {payload}"); Debug.Log($"MQTT OnTopic {topic.Filter.OriginalFilter} => {payload}");
ServerLog.LogMqtt(MQTTDomain, MQTTPort.ToString(), topic.Filter.OriginalFilter, payload, DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ")); ServerLog.LogMqtt(MQTTDomain, MQTTPort.ToString(), topic.Filter.OriginalFilter, payload, DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"));
if (onMessageReceived != null)
if(topicHandler.TryGetValue(topic.Filter.OriginalFilter, out var handler))
{ {
onMessageReceived.Invoke(topic.Filter.OriginalFilter, payload); // 등록된 핸들러가 있으면 호출
handler.Invoke(topic.Filter.OriginalFilter, payload);
} }
} }

View File

@@ -0,0 +1,602 @@
#nullable enable
using Cysharp.Threading.Tasks;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using UnityEngine;
using UVC.Data;
namespace UVC.Tests.Data
{
[TestFixture]
public class MQTTPipeLineTests
{
private MQTTPipeLine pipeLine;
private Dictionary<string, TestDataHandler> handlers;
private Dictionary<string, DataMask> dataMasks;
private readonly string[] topicNames = { "AGV", "CARRIER", "STOCKER_STACK", "ALL" };
[SetUp]
public void Setup()
{
// 기본 테스트 환경 설정
pipeLine = new MQTTPipeLine("localhost", 1883);
pipeLine.UseMockup = true; // 테스트에서는 항상 MockMQTTService 사용
// 핸들러와 데이터 마스크 초기화
handlers = new Dictionary<string, TestDataHandler>();
dataMasks = new Dictionary<string, DataMask>();
// 각 토픽별 핸들러와 데이터 마스크 설정
foreach (var topic in topicNames)
{
handlers[topic] = new TestDataHandler();
dataMasks[topic] = CreateDataMaskForTopic(topic);
}
}
public async UniTask TestAll()
{
Setup();
Debug.Log("===== MQTTPipeLine 테스트 시작 =====");
await RunTestAsync(nameof(ExecutePipeLine_AllTopics_RegistersAndHandlesMessages), ExecutePipeLine_AllTopics_RegistersAndHandlesMessages);
await RunTestAsync(nameof(RemoveTopic_ShouldStopReceivingMessages), RemoveTopic_ShouldStopReceivingMessages);
await RunTestAsync(nameof(UpdatedDataOnly_ShouldOnlyCallHandlerForUpdatedData), UpdatedDataOnly_ShouldOnlyCallHandlerForUpdatedData);
await RunTestAsync(nameof(UpdatedDataOnly_WithMockMQTTService_ShouldOnlyReceiveUpdatedData), UpdatedDataOnly_WithMockMQTTService_ShouldOnlyReceiveUpdatedData);
RunTest(nameof(OnTopicMessage_ValidJsonObject_CallsHandler), OnTopicMessage_ValidJsonObject_CallsHandler);
RunTest(nameof(OnTopicMessage_JsonArray_CallsHandler), OnTopicMessage_JsonArray_CallsHandler);
RunTest(nameof(OnTopicMessage_EmptyMessage_DoesNotCallHandler), OnTopicMessage_EmptyMessage_DoesNotCallHandler);
RunTest(nameof(OnTopicMessage_InvalidJson_DoesNotCallHandler), OnTopicMessage_InvalidJson_DoesNotCallHandler);
Debug.Log("===== MQTTPipeLine 테스트 완료 =====");
}
private void RunTest(string testName, Action testAction)
{
try
{
Debug.Log($"테스트 시작: {testName}");
testAction();
Debug.Log($"테스트 성공: {testName}");
}
catch (Exception ex)
{
Debug.LogError($"테스트 실패: {testName}\n{ex.Message}\n{ex.StackTrace}");
}
}
private async UniTask RunTestAsync(string testName, Func<UniTask> testAction)
{
try
{
Debug.Log($"테스트 시작: {testName}");
await testAction();
Debug.Log($"테스트 성공: {testName}");
}
catch (Exception ex)
{
Debug.LogError($"테스트 실패: {testName}\n{ex.Message}\n{ex.StackTrace}");
}
}
[TearDown]
public void TearDown()
{
// 테스트 완료 후 리소스 정리
pipeLine.Stop();
pipeLine.Dispose();
}
private DataMask CreateDataMaskForTopic(string topic)
{
var dataMask = new DataMask();
dataMask.ObjectName = topic;
// 각 토픽별로 ObjectIdKey 설정
switch (topic)
{
case "AGV":
dataMask.ObjectIdKey = "VHL_NAME";
dataMask["VHL_NAME"] = "HFF09CNA8013";
dataMask["AGV_IDX"] = 12;
dataMask["B_INSTALL"] = "Y";
dataMask["NODE_ID"] = 235;
dataMask["REAL_ID"] = 235;
dataMask["VHL_STATE"] = 11;
dataMask["BAY_LIST"] = "21;";
dataMask["X"] = 118301;
dataMask["Y"] = 20177;
dataMask["MODE"] = 1;
dataMask["BATT"] = 75;
dataMask["SUB_GOAL"] = 211;
dataMask["FINAL_GOAL"] = 1006;
dataMask["TIMESTAMP"] = "2025-03-25T12:00:00.980Z";
dataMask["DEGREE"] = 181.2;
dataMask["STOP_STATE"] = 0;
dataMask["JOB_ID"] = "2F24217_289_7038296224059039";
dataMask["DESTINATION_PORT"] = "HFF09MPI0200_LIP01";
dataMask["SOURCE_PORT"] = "HFF09AGM0100_UOP01";
dataMask["FROM"] = "HFF09AGM0100,NULL,0201012";
dataMask["TO"] = "HFF09MPI0200,HFF09MPI0200_LIP01,NULL";
dataMask["TRANSPORT_JOB_TIMESTAMP"] = "2025-03-25T05:40:19.000Z";
dataMask["FACTOR"] = 69.3;
dataMask["AGV_FACTOR_TIMESTAMP"] = DateTime.Now;
break;
case "CARRIER":
dataMask.ObjectIdKey = "MAIN_CARR_ID";
dataMask["MAIN_CARR_ID"] = "2F02365";
dataMask["SUB_CARR_ID"] = "2F02365,2F70671,2F28723";
dataMask["CARR_SEQ"] = "3";
dataMask["CARR_USE"] = "EMPTY";
dataMask["CURRENTPORT"] = "HFF09CNV0300_ABP3003";
dataMask["MOVESTATUS"] = "ARRIVED";
dataMask["MOVEFLAG"] = "0";
dataMask["PROD_DETAIL_CODE"] = "E3A";
dataMask["ASSIGN_LOT_QTY"] = "0";
dataMask["CARR_SIZE_TYPE"] = "STACK3";
dataMask["ABNM_VALUE"] = "0";
dataMask["LINE_ID"] = "FM0I";
dataMask["TIMESTAMP"] = DateTime.Now;
dataMask["GOOD_QTY"] = "0";
dataMask["CURRENTLOCATION"] = "HFF09CNV0300";
break;
case "STOCKER_STACK":
dataMask.ObjectIdKey = "STOCKER_NAME";
dataMask["STOCKER_NAME"] = "HFF09AGN0300";
dataMask["CAPACITY"] = "89.57";
dataMask["MAXIMUM_CAPACITY"] = "834";
dataMask["TRAY_CAPACITY"] = "83.25";
dataMask["MAXIMUM_TRAY_CAPACITY"] = "2502";
dataMask["RACK_LOAD_COUNT"] = "747";
dataMask["RACK_EMPTY_COUNT"] = "87";
dataMask["RESERVATED_RETURN_COUNT"] = "5";
dataMask["TRAY_COUNT"] = "2083";
dataMask["TRAY_REWORK_COUNT_AVG"] = "3";
dataMask["TRAY_REWORK_COUNT_MAX"] = "153";
dataMask["TRAY_REWORK_COUNT_MIN"] = "0";
dataMask["RACK_DISABLE_COUNT"] = "4";
dataMask["KOR_EQP_NAME"] = "상온Aging #03";
dataMask["ENG_EQP_NAME"] = "상온Aging #03";
dataMask["TIMESTAMP"] = DateTime.Now;
dataMask["STEP"] = new JArray
{
new DataObject {
["STOCKER_NAME"] = "HFF09AGN0300",
["STEP_ID"] = "8106",
["RACK_STEP_COUNT"] = "88",
["TOTAL"] = "834",
["STEP_CAPACITY"] = "10.55",
},
};
break;
case "ALL":
// ALL 토픽은 ObjectIdKey 없음
dataMask["AGV"] = new JArray()
{
new DataObject
{
["VHL_NAME"] = "HFF09CNA8053",
["AGV_IDX"] = 52,
["X"] = 223316,
["Y"] = 218171,
["B_INSTALL"] = "Y",
["NODE_ID"] = 235,
["REAL_ID"] = 235,
["VHL_STATE"] = 11,
["BAY_LIST"] = "21;",
["MODE"] = 1,
["BATT"] = 75,
["SUB_GOAL"] = 211,
["FINAL_GOAL"] = 1006,
["TIMESTAMP"] = "2025-03-25T12:00:00.980Z",
["DEGREE"] = 181.2,
["STOP_STATE"] = 0,
["JOB_ID"] = "2F24217_289_7038296224059039",
["DESTINATION_PORT"] = "HFF09MPI0200_LIP01",
["SOURCE_PORT"] = "HFF09AGM0100_UOP01",
["FROM"] = "HFF09AGM0100,NULL,0201012",
["TO"] = "HFF09MPI0200,HFF09MPI0200_LIP01,NULL",
["TRANSPORT_JOB_TIMESTAMP"] = "2025-03-25T05:40:19.000Z",
["FACTOR"] = 69.3,
}
};
dataMask["CARRIER"] = new JArray()
{
new DataObject
{
["MAIN_CARR_ID"] = "2F02365",
["SUB_CARR_ID"] = "2F02365,2F70671,2F28723",
["CARR_SEQ"] = "3",
["CARR_USE"] = "EMPTY",
["CURRENTPORT"] = "HFF09CNV0300_ABP3003",
["MOVESTATUS"] = "ARRIVED",
["MOVEFLAG"] = "0",
["PROD_DETAIL_CODE"] = "E3A",
["ASSIGN_LOT_QTY"] = "0",
["CARR_SIZE_TYPE"] = "STACK3",
["ABNM_VALUE"] = "0",
["LINE_ID"] = "FM0I",
["TIMESTAMP"] = DateTime.Now,
["GOOD_QTY"] = "0",
["CURRENTLOCATION"] = "HFF09CNV0300",
}
};
dataMask["CARRIER"] = new JArray()
{
new DataObject
{
["STOCKER_NAME"] = "HFF09AGN0300",
["CAPACITY"] = "89.57",
["MAXIMUM_CAPACITY"] = "834",
["TRAY_CAPACITY"] = "83.25",
["MAXIMUM_TRAY_CAPACITY"] = "2502",
["RACK_LOAD_COUNT"] = "747",
["RACK_EMPTY_COUNT"] = "87",
["RESERVATED_RETURN_COUNT"] = "5",
["TRAY_COUNT"] = "2083",
["TRAY_REWORK_COUNT_AVG"] = "3",
["TRAY_REWORK_COUNT_MAX"] = "153",
["TRAY_REWORK_COUNT_MIN"] = "0",
["RACK_DISABLE_COUNT"] = "4",
["KOR_EQP_NAME"] = "상온Aging #03",
["ENG_EQP_NAME"] = "상온Aging #03",
["TIMESTAMP"] = DateTime.Now,
["STEP"] = new JArray
{
new DataObject {
["STOCKER_NAME"] = "HFF09AGN0300",
["STEP_ID"] = "8106",
["RACK_STEP_COUNT"] = "88",
["TOTAL"] = "834",
["STEP_CAPACITY"] = "10.55",
},
}
}
};
break;
}
return dataMask;
}
[Test]
public async UniTask ExecutePipeLine_AllTopics_RegistersAndHandlesMessages()
{
// Arrange - 파이프라인 설정
foreach (var topic in topicNames)
{
// 필요한 UpdatedDataOnly 설정
bool updatedDataOnly = topic != "ALL";
var pipelineInfo = new MQTTPipeLineInfo(topic, updatedDataOnly)
.setDataMapper(new DataMapper(dataMasks[topic]))
.setHandler(handlers[topic].HandleData);
pipeLine.Add(pipelineInfo);
}
// Act - 파이프라인 실행
pipeLine.Execute();
// Assert - 일정 시간 기다린 후 각 핸들러가 호출되었는지 확인
await UniTask.Delay(1500);
// 각 토픽별로 핸들러가 호출되었는지 확인
foreach (var topic in topicNames)
{
Assert.IsTrue(handlers[topic].CallCount > 0, $"{topic} 토픽의 핸들러가 호출되지 않았습니다.");
if (topic != "ALL")
{
// ALL을 제외한 토픽은 ObjectIdKey가 설정되어 있어야 함
Assert.IsNotNull(handlers[topic].LastDataObject, $"{topic} 토픽의 핸들러에 전달된 DataObject가 null입니다.");
}
}
}
[Test]
public async UniTask RemoveTopic_ShouldStopReceivingMessages()
{
// Arrange
// AGV 토픽만 등록
var agvInfo = new MQTTPipeLineInfo("AGV", true)
.setDataMapper(new DataMapper(dataMasks["AGV"]))
.setHandler(handlers["AGV"].HandleData);
pipeLine.Add(agvInfo);
pipeLine.Execute();
// 메시지가 수신되도록 잠시 대기
await UniTask.Delay(1000);
// 초기 호출 횟수 저장
int initialCallCount = handlers["AGV"].CallCount;
Assert.IsTrue(initialCallCount > 0, "초기 AGV 토픽의 핸들러가 호출되지 않았습니다.");
// Act
pipeLine.Remove("AGV"); // AGV 토픽 제거
// 핸들러 초기화
handlers["AGV"].Reset();
// 충분한 시간 대기
await UniTask.Delay(1500);
// Assert
Assert.AreEqual(0, handlers["AGV"].CallCount, "토픽을 제거했지만 핸들러가 여전히 호출되고 있습니다.");
}
[Test]
public async UniTask UpdatedDataOnly_ShouldOnlyCallHandlerForUpdatedData()
{
// Arrange - 파이프라인 설정
// TestMQTTPipeLine을 사용하여 직접 메시지를 보낼 수 있게 함
var testPipeLine = new TestMQTTPipeLine();
// UpdatedDataOnly가 true인 AGV 토픽 추가
var agvInfo = new MQTTPipeLineInfo("AGV", true)
.setDataMapper(new DataMapper(dataMasks["AGV"]))
.setHandler(handlers["AGV"].HandleData);
testPipeLine.Add(agvInfo);
// Mock 데이터 생성 (업데이트가 있는 데이터)
string jsonWithUpdates = "{\"VHL_NAME\":\"HFF09CNA8053\",\"AGV_IDX\":52,\"X\":223316,\"Y\":218171}";
// Act
// 첫 번째 메시지 전송 (초기 데이터)
testPipeLine.TestOnTopicMessage("AGV", jsonWithUpdates);
// 동일한 메시지 다시 전송 (변경 없음)
testPipeLine.TestOnTopicMessage("AGV", jsonWithUpdates);
// Assert
// UpdatedDataOnly가 true이므로 두 번째 메시지는 핸들러를 호출하지 않아야 함
// 실제로는 DataRepository가 필요하므로 완전한 테스트는 어려움
// 여기서는 최소한 첫 번째 메시지가 처리되었는지 확인
Assert.IsTrue(handlers["AGV"].CallCount > 0, "AGV 토픽의 핸들러가 호출되지 않았습니다.");
}
[Test]
public void OnTopicMessage_ValidJsonObject_CallsHandler()
{
// Arrange
var testPipeLine = new TestMQTTPipeLine();
foreach (var topic in topicNames)
{
bool updatedDataOnly = topic != "ALL";
var pipelineInfo = new MQTTPipeLineInfo(topic, updatedDataOnly)
.setDataMapper(new DataMapper(dataMasks[topic]))
.setHandler(handlers[topic].HandleData);
testPipeLine.Add(pipelineInfo);
}
// Act
// 각 토픽에 대해 유효한 JSON 메시지 전송
testPipeLine.TestOnTopicMessage("AGV", "{\"VHL_NAME\":\"HFF09CNA8053\",\"AGV_IDX\":52}");
testPipeLine.TestOnTopicMessage("CARRIER", "{\"MAIN_CARR_ID\":\"2F02365\",\"SUB_CARR_ID\":\"2F02365,2F70671,2F28723\"}");
testPipeLine.TestOnTopicMessage("STOCKER_STACK", "{\"STOCKER_NAME\":\"HFF09AGN0300\",\"CAPACITY\":\"89.57\"}");
testPipeLine.TestOnTopicMessage("ALL", "{\"key\":\"value\"}");
// Assert
// 각 토픽의 핸들러가 호출되었는지 확인
foreach (var topic in topicNames)
{
Assert.AreEqual(1, handlers[topic].CallCount, $"{topic} 토픽의 핸들러가 정확히 한 번 호출되어야 합니다.");
}
}
[Test]
public void OnTopicMessage_JsonArray_CallsHandler()
{
// Arrange
var testPipeLine = new TestMQTTPipeLine();
var pipelineInfo = new MQTTPipeLineInfo("AGV", true)
.setDataMapper(new DataMapper(dataMasks["AGV"]))
.setHandler(handlers["AGV"].HandleData);
testPipeLine.Add(pipelineInfo);
// JSON 배열 메시지 생성
string jsonArrayMessage =
"[{\"VHL_NAME\":\"HFF09CNA8053\",\"AGV_IDX\":52},{\"VHL_NAME\":\"HFF09CNA8033\",\"AGV_IDX\":32}]";
// Act
testPipeLine.TestOnTopicMessage("AGV", jsonArrayMessage);
// Assert
Assert.AreEqual(1, handlers["AGV"].CallCount, "AGV 토픽의 핸들러가 정확히 한 번 호출되어야 합니다.");
Assert.IsNotNull(handlers["AGV"].LastDataObject, "핸들러에 전달된 DataObject가 null입니다.");
}
[Test]
public void OnTopicMessage_EmptyMessage_DoesNotCallHandler()
{
// Arrange
var testPipeLine = new TestMQTTPipeLine();
var pipelineInfo = new MQTTPipeLineInfo("AGV", true)
.setDataMapper(new DataMapper(dataMasks["AGV"]))
.setHandler(handlers["AGV"].HandleData);
testPipeLine.Add(pipelineInfo);
// Act
testPipeLine.TestOnTopicMessage("AGV", ""); // 빈 메시지 전송
// Assert
Assert.AreEqual(0, handlers["AGV"].CallCount, "빈 메시지가 전달되었을 때 핸들러가 호출되지 않아야 합니다.");
}
[Test]
public void OnTopicMessage_InvalidJson_DoesNotCallHandler()
{
// Arrange
var testPipeLine = new TestMQTTPipeLine();
var pipelineInfo = new MQTTPipeLineInfo("AGV", true)
.setDataMapper(new DataMapper(dataMasks["AGV"]))
.setHandler(handlers["AGV"].HandleData);
testPipeLine.Add(pipelineInfo);
// Act - 잘못된 JSON 형식의 메시지 전송
testPipeLine.TestOnTopicMessage("AGV", "{invalid json}");
// Assert
Assert.AreEqual(0, handlers["AGV"].CallCount, "잘못된 JSON 형식의 메시지가 전달되었을 때 핸들러가 호출되지 않아야 합니다.");
}
[Test]
public async UniTask UpdatedDataOnly_WithMockMQTTService_ShouldOnlyReceiveUpdatedData()
{
// Arrange
// 1. 데이터 핸들러를 통해 수신된 데이터 추적
var handler = new UpdatedDataTrackingHandler();
// 2. AGV 토픽을 위한 데이터 마스크 설정
var dataMask = new DataMask();
dataMask.ObjectName = "AGV";
dataMask.ObjectIdKey = "VHL_NAME";
// 3. MQTTPipeLine 설정 (MockMQTTService 사용)
var pipeline = new MQTTPipeLine("localhost", 1883);
pipeline.UseMockup = true; // MockMQTTService 사용 설정
// 4. UpdatedDataOnly=true로 토픽 등록
var pipelineInfo = new MQTTPipeLineInfo("AGV", true)
.setDataMapper(new DataMapper(dataMask))
.setHandler(handler.HandleData);
pipeline.Add(pipelineInfo);
// Act
// 파이프라인 실행 - 이것이 MockMQTTService를 통해 메시지를 보내기 시작
pipeline.Execute();
// 첫 번째 데이터 세트가 수신될 때까지 대기
await UniTask.Delay(1500);
// 첫 번째 데이터 세트의 콜백 수를 기록
int initialCallCount = handler.CallCount;
Assert.IsTrue(initialCallCount > 0, "첫 번째 메시지 세트가 수신되지 않았습니다.");
// AGV 항목 수 저장
int initialAgvCount = handler.ReceivedAgvItems.Count;
Assert.IsTrue(initialAgvCount > 0, "첫 번째 메시지에 AGV 항목이 없습니다.");
// 첫 번째 콜백에서 수신된 업데이트 항목 수 기록
int firstUpdatedCount = handler.LastUpdatedCount;
Assert.IsTrue(firstUpdatedCount > 0, "첫 번째 메시지에 업데이트된 데이터가 없습니다.");
// 다음 데이터 세트가 도착하기를 기다림
await UniTask.Delay(1500);
// 두 번째 메시지가 도착했는지 확인
int finalCallCount = handler.CallCount;
Assert.IsTrue(finalCallCount > initialCallCount, "두 번째 메시지 세트가 수신되지 않았습니다.");
// Assert
// 기본 검사: 모든 호출에서 업데이트된 데이터만 전송되었는지 확인
foreach (int updatedCount in handler.UpdatedCounts)
{
Assert.IsTrue(updatedCount > 0, "업데이트된 데이터가 없는 콜백이 있습니다.");
}
// 두 번째 메시지에서 처음 도착한 AGV 항목과 추가/변경된 항목이 있는지 확인
// MockMQTTService는 매번 다른 데이터 세트를 보내므로 이런 차이가 있어야 함
bool hasChanges = handler.ReceivedAgvItems.Count > initialAgvCount ||
handler.HasUpdatedExistingItems;
Assert.IsTrue(hasChanges, "두 번째 메시지에서 변경된 데이터가 감지되지 않았습니다.");
// 정리
pipeline.Stop();
pipeline.Dispose();
}
// UpdatedDataOnly 테스트를 위한 특수 핸들러
public class UpdatedDataTrackingHandler
{
public int CallCount { get; private set; } = 0;
public List<int> UpdatedCounts { get; private set; } = new List<int>();
public int LastUpdatedCount { get; private set; } = 0;
public HashSet<string> ReceivedAgvItems { get; private set; } = new HashSet<string>();
public bool HasUpdatedExistingItems { get; private set; } = false;
public void HandleData(IDataObject? dataObject)
{
CallCount++;
if (dataObject != null)
{
// 업데이트 개수 기록
LastUpdatedCount = dataObject.UpdatedCount;
UpdatedCounts.Add(dataObject.UpdatedCount);
// AGV 데이터 분석
if (dataObject is DataArray agvData)
{
foreach (var item in agvData)
{
if (item is DataObject obj && obj.Name == "AGV" && obj["VHL_NAME"] != null)
{
string vhlName = obj["VHL_NAME"].ToString();
// 이미 존재하는 항목이 업데이트된 경우
if (ReceivedAgvItems.Contains(vhlName))
{
HasUpdatedExistingItems = true;
}
// 새로운 항목 추가
ReceivedAgvItems.Add(vhlName);
}
}
}
}
}
}
}
// MQTTPipeLine의 OnTopicMessage 메서드를 테스트하기 위한 확장 클래스
public class TestMQTTPipeLine : MQTTPipeLine
{
public TestMQTTPipeLine() : base("localhost", 1883)
{
UseMockup = true;
}
public void TestOnTopicMessage(string topic, string message)
{
// private 메서드에 접근하기 위한 래퍼
typeof(MQTTPipeLine).GetMethod("OnTopicMessage",
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance)?.Invoke(this, new object[] { topic, message });
}
}
// 테스트용 데이터 핸들러 클래스
public class TestDataHandler
{
public int CallCount { get; private set; } = 0;
public IDataObject? LastDataObject { get; private set; } = null;
public void HandleData(IDataObject? dataObject)
{
CallCount++;
LastDataObject = dataObject;
}
public void Reset()
{
CallCount = 0;
LastDataObject = null;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5d12a5bc2caf7f440b9a294cdf18cb4a

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ed55ad7bcf744a34b9da619859525801

View File

@@ -7,7 +7,8 @@ namespace UVC.Tests
public static void RunAllTests() public static void RunAllTests()
{ {
//new DataMapperTests().TestAll(); //new DataMapperTests().TestAll();
new HttpPipeLineTests().TestAll(); //new HttpPipeLineTests().TestAll();
new MQTTPipeLineTests().TestAll();
} }
} }
} }