아모센스 마킹 gui
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

268 lines
15 KiB

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<string>();
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<string> Commands { get; } = new List<string>();
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<string> QueryAsync(string command) { Commands.Add(command); return Task.FromResult(_response); }
public void Dispose() { }
}
}