using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; using marking_gui.Services; using marking_gui.Models; internal static class Program { private static readonly string DiagnosticPath = Path.Combine(AppContext.BaseDirectory, "debug"); private static readonly string DailyPath = Path.Combine(AppContext.BaseDirectory, "Logs"); private static readonly string FixturePath = Path.Combine(AppContext.BaseDirectory, "fixtures", Guid.NewGuid().ToString("N")); private static async Task Main() { Directory.CreateDirectory(FixturePath); TestErrorReport(); TestDirectoryRecreation(); TestIndependentWrites(); await TestConcurrentErrors(); TestBoundedHistory(); await TestMeasurementFailure(false); await TestMeasurementFailure(true); await TestNormalMeasurement(); await TestOverloadCombinations(); await TestOverloadFormats(); await TestInvalidResponses(); TestDatabaseValues(); TestOfflineNgSequence(); Console.WriteLine("PASS: diagnostics, 7 overload combinations, overload formats, invalid responses, DB parameters and NG no-save/sequence (no physical equipment/DB used)."); Console.WriteLine("Diagnostic samples: " + DiagnosticPath); } private static void TestErrorReport() { string token = Guid.NewGuid().ToString("N"); LoggerService.Info("검사 시작 " + token); Exception failure; try { throw new InvalidOperationException("outer " + token, new FormatException("inner 原因")); } catch (Exception ex) { failure = ex; } string report = CaptureReport(() => LoggerService.Error("측정 실패 " + token, failure, "원문=+9.90000000E+37")); Require(report.Contains("검사 시작 " + token), "Recent context is missing."); Require(report.Contains("FormatException: inner 原因"), "Inner exception/UTF-8 was lost."); Require(report.Contains("Program.TestErrorReport"), "Stack trace is missing."); Require(report.Contains("원문=+9.90000000E+37"), "Raw measurement context is missing."); Require(File.ReadAllText(DailyFile()).Contains("[진단 파일]"), "Daily log must point to a diagnostic file."); CaptureReport(() => LoggerService.Error("Exception 객체 없는 오류")); } private static void TestDirectoryRecreation() { Directory.Move(DiagnosticPath, Path.Combine(FixturePath, "previous-diagnostics")); Directory.Move(DailyPath, Path.Combine(FixturePath, "previous-daily")); string report = CaptureReport(() => LoggerService.Error("폴더 자동 재생성")); Require(report.Contains("폴더 자동 재생성") && File.Exists(DailyFile()), "Deleted directories were not recreated."); } private static void TestIndependentWrites() { Directory.Move(DailyPath, Path.Combine(FixturePath, "daily-block-test")); File.WriteAllText(DailyPath, "test-owned blocker"); try { CaptureReport(() => LoggerService.Error("일별 로그 저장 불가")); } finally { File.Delete(DailyPath); } Directory.Move(DiagnosticPath, Path.Combine(FixturePath, "diagnostic-block-test")); File.WriteAllText(DiagnosticPath, "test-owned blocker"); try { LoggerService.Error("진단 폴더 저장 불가"); Require(File.ReadAllText(DailyFile()).Contains("[진단 파일 저장 실패]"), "Diagnostic failure was not recorded in the daily log."); } finally { File.Delete(DiagnosticPath); } } private static async Task TestConcurrentErrors() { Directory.CreateDirectory(DiagnosticPath); int before = Directory.GetFiles(DiagnosticPath, "*.log").Length; await Task.WhenAll(Enumerable.Range(0, 24).Select(i => Task.Run(() => LoggerService.Error("concurrent " + i)))); Require(Directory.GetFiles(DiagnosticPath, "*.log").Length == before + 24, "Concurrent reports were overwritten or lost."); } private static void TestBoundedHistory() { string oldToken = Guid.NewGuid().ToString("N"); LoggerService.Info(oldToken); for (int i = 0; i < 105; i++) LoggerService.Info("history " + i); string report = CaptureReport(() => LoggerService.Error("bounded history")); Require(!report.Contains(oldToken) && report.Contains("history 104"), "Recent history is not bounded correctly."); } private static async Task TestMeasurementFailure(bool cleanupFails) { var psu = new FakeClient("0.008", cleanupFails); using var service = CreateEquipment(psu, new FakeClient("4.5"), new FakeClient("invalid-response")); var before = Reports(); Exception failure = null; try { await service.MeasureAsync(); } catch (Exception ex) { failure = ex; } Require(failure is InvalidOperationException && failure.Message.Contains("RESISTANCE"), "Original parsing exception was changed/lost."); Require(psu.Commands.Contains("OUTP OFF, (@1)"), "PSU cleanup was not attempted."); string[] reports = Reports().Except(before).Select(File.ReadAllText).ToArray(); string report = reports.Single(r => r.Contains("[오류 상세]" + Environment.NewLine + "[Equipment] 계측 시퀀스 실패")); Require(report.Contains("invalid-response") && report.Contains("MEAS:RES?") && report.Contains("test-resistance:5025"), "Report lost the endpoint, command or raw response."); Require(report.Contains(cleanupFails ? "PSU OUTP OFF FAILED" : "PSU CLEANUP 명령 전송 완료"), "Cleanup outcome is missing."); if (cleanupFails) Require(reports.Any(r => r.Contains("IOException: simulated cleanup failure")), "Cleanup failure diagnostic is missing."); } private static async Task TestNormalMeasurement() { var psu = new FakeClient("0.008"); using var service = CreateEquipment(psu, new FakeClient("4.5"), new FakeClient("10000")); var before = Reports(); var result = await service.MeasureAsync(); Require(result.Voltage == 4.5 && result.Current == 8 && result.Resistance == 10, "Normal measurement results changed."); Require(Reports().Length == before.Length, "Successful measurement created an error report."); Require(psu.Commands.Contains("OUTP OFF, (@1)"), "Normal PSU cleanup changed."); } private static async Task TestOverloadCombinations() { for (int mask = 1; mask < 8; mask++) { bool voltageOl = (mask & 1) != 0; bool currentOl = (mask & 2) != 0; bool resistanceOl = (mask & 4) != 0; var psu = new FakeClient(currentOl ? "+9.90000000E+37" : "0.008"); var voltage = new FakeClient(voltageOl ? "+9.90000000E+37" : "4.5"); var resistance = new FakeClient(resistanceOl ? "+9.90000000E+37" : "10000"); using var service = CreateEquipment(psu, voltage, resistance); var updates = new List<(double? Voltage, double? Current, double? Resistance)>(); var before = Reports(); var result = await service.MeasureAsync((v, c, r) => updates.Add((v, c, r))); Require(result.Voltage == (voltageOl ? 999 : 4.5), "Voltage overload changed another reading."); Require(result.Current == (currentOl ? 999 : 8), "Current overload sentinel was scaled or other current lost."); Require(result.Resistance == (resistanceOl ? 999 : 10), "Resistance overload sentinel was scaled or normal resistance lost."); Require(result.VoltageOverloaded == voltageOl && result.CurrentOverloaded == currentOl && result.ResistanceOverloaded == resistanceOl, "Per-channel overload flags are incorrect."); Require(result.HasOverload && updates.Count == 3, "Overload interrupted the remaining readings/UI updates."); Require(updates[2].Voltage == result.Voltage && updates[2].Current == result.Current && updates[2].Resistance == result.Resistance, "UI update does not preserve all final readings."); Require(psu.Commands.Contains("MEAS:CURR? (@1)") && psu.Commands.Contains("OUTP OFF, (@1)"), "Overload skipped current measurement or cleanup."); Require(resistance.Commands.Contains("MEAS:RES?"), "Voltage overload skipped resistance."); string report = Reports().Except(before).Select(File.ReadAllText).Single(); Require(report.Contains("Overload 항목을 999") && report.Contains("+9.90000000E+37"), "Overload diagnostic is missing."); var parameters = DatabaseService.CreateMeasurementParameters(result.Voltage, result.Current, result.Resistance); Require((decimal)parameters[0].Value == (decimal)result.Voltage && (decimal)parameters[1].Value == (decimal)result.Current && (decimal)parameters[2].Value == (decimal)result.Resistance, "DB parameters changed the measured values."); } } private static async Task TestOverloadFormats() { foreach (string response in new[] { "OL", " o.l\r\n", "OVERLOAD", "ovld", "-9.90000000E+37" }) { using var service = CreateEquipment(new FakeClient(response), new FakeClient(response), new FakeClient(response)); var result = await service.MeasureAsync(); Require(result.Voltage == 999 && result.Current == 999 && result.Resistance == 999 && result.HasOverload, "Overload response was not recognized: " + response); } using var normalService = CreateEquipment(new FakeClient("0"), new FakeClient("0"), new FakeClient("999")); var normal = await normalService.MeasureAsync(); Require(normal.Voltage == 0 && normal.Current == 0 && normal.Resistance == 0.999 && !normal.HasOverload, "Real zero/999 Ohm measurement was mistaken for overload."); } private static async Task TestInvalidResponses() { foreach (string response in new[] { "", "bad", "NaN", "INF", "1E309" }) { var psu = new FakeClient("0.008"); using var service = CreateEquipment(psu, new FakeClient("4.5"), new FakeClient(response)); Exception failure = null; try { await service.MeasureAsync(); } catch (Exception ex) { failure = ex; } Require(failure is InvalidOperationException, "Malformed/empty response must not be silently saved as overload."); Require(psu.Commands.Contains("OUTP OFF, (@1)"), "Bad response bypassed PSU cleanup."); } } private static void TestDatabaseValues() { var parameters = DatabaseService.CreateMeasurementParameters(999, 999, 999); Require(parameters.All(p => p.Precision == 5 && p.Scale == 2 && (decimal)p.Value == 999m), "999 does not fit the configured database parameters."); var normal = DatabaseService.CreateMeasurementParameters(4.567, 8.126, 12.346); Require((decimal)normal[0].Value == 4.57m && (decimal)normal[1].Value == 8.13m && (decimal)normal[2].Value == 12.35m, "Normal measurements were not rounded for decimal(5,2)."); } private static void TestOfflineNgSequence() { string settings = Path.Combine(AppContext.BaseDirectory, "Database.ini"); Require(!File.Exists(settings), "Refusing to overwrite an existing test Database.ini."); File.WriteAllText(settings, "[Database]\nOfflineMode=true\n"); try { var database = new DatabaseService(); Require(database.OfflineMode, "Test database must be offline before saving."); SaveOffline(database, "NG", "LOT0009"); Require(database.GetNextMarkingSequence("LOT") == 1, "Overload NG consumed a marking sequence."); SaveOffline(database, "OK", "LOT0010"); SaveOffline(database, "NG", "LOT0020"); Require(database.GetNextMarkingSequence("LOT") == 11, "NG reinspection changed the issued sequence."); } finally { File.Delete(settings); } } private static void SaveOffline(DatabaseService database, string result, string serial) { bool saved = database.SaveProductResult("TEST_IC", "maker", "model", "", "", "operator", "line", "LOT", "jig", result == "NG" ? 999 : 4.5, 8, 10, result, DateTime.Now, DateTime.Now, serial, "pcb"); Require(saved == (result == "OK"), "Only OK results may be saved; NG must be skipped."); } private static EquipmentMeasurementService CreateEquipment(FakeClient psu, FakeClient voltage, FakeClient resistance) { string settings = Path.Combine(FixturePath, "test-settings.ini"); File.WriteAllText(settings, "[Equipment]\nSettleMilliseconds=0\nTimeout=100\n[DMM]\nVoltageConnection=LAN\nVoltageHost=test-voltage\n[ResistanceDMM]\nResistanceDMMConnection=LAN\nResistanceDMMHost=test-resistance\n[PowerSupply]\nCurrentConnection=LAN\nCurrentHost=test-psu\n"); var service = new EquipmentMeasurementService(settings); SetClient(service, "_psuClient", psu); SetClient(service, "_voltageDmmClient", voltage); SetClient(service, "_resistanceDmmClient", resistance); return service; } private static void SetClient(EquipmentMeasurementService service, string field, IScpiClient client) => typeof(EquipmentMeasurementService).GetField(field, BindingFlags.Instance | BindingFlags.NonPublic).SetValue(service, client); private static string[] Reports() => Directory.Exists(DiagnosticPath) ? Directory.GetFiles(DiagnosticPath, "*.log") : Array.Empty(); private static string DailyFile() => Path.Combine(DailyPath, DateTime.Now.ToString("yyyy-MM-dd") + ".log"); private static string CaptureReport(Action write) { string[] before = Reports(); write(); string reportPath = Reports().Except(before).Single(); string fileName = Path.GetFileName(reportPath); Require(fileName.StartsWith("error_", StringComparison.Ordinal) && fileName.All(c => c <= 127), "Diagnostic filenames must use the English prefix and ASCII characters only."); return File.ReadAllText(reportPath); } private static void Require(bool condition, string message) { if (!condition) throw new Exception(message); } private sealed class FakeClient : IScpiClient { private readonly string _response; private readonly bool _cleanupFails; public List Commands { get; } = new List(); public FakeClient(string response, bool cleanupFails = false) { _response = response; _cleanupFails = cleanupFails; } public Task SendAsync(string command) { Commands.Add(command); return _cleanupFails && command.Contains("OFF") ? Task.FromException(new IOException("simulated cleanup failure")) : Task.CompletedTask; } public Task QueryAsync(string command) { Commands.Add(command); return Task.FromResult(_response); } public void Dispose() { } } }