using System.Reflection; using GitConverter.TestsApp.TestSupport; namespace GitConverter.TestsApp.ConsoleApp { /// /// Integration tests that exercise the console Program.Run(...) end-to-end. /// /// /// Purpose /// - Run the shipped ConsoleApp entry point (Program.Run) via reflection so tests are not terminated by Environment.Exit. /// - Execute end-to-end conversion scenarios using small sample artifacts placed under the TestData folders /// (TestData\Shapefile, TestData\Csv, TestData\Gml). Sample artifacts must be copied to the test output /// (see TestData README and csproj CopyToOutputDirectory settings). /// /// Behavior & expectations /// - Tests are defensive: they skip when required artifacts are missing so CI remains non-fatal when private samples are absent. /// - For each sample file/archive the tests create isolated output and temp folders under a unique temp root. /// - When Program.Run returns exit code 0 the test asserts that at least one output file exists in the output folder. /// - When Program.Run returns non-zero the test asserts that console output (stdout+stderr) contains diagnostic text. /// /// Implementation notes /// - Program.Run is located by reflection. The helper InvokeRun implements robust probing of the test runtime folder /// and common project output locations so tests work both inside Visual Studio and CI build outputs. /// - Console StdOut/Err are captured and restored during the test lifetime to allow assertions on the output. /// - Temporary folders are deleted in the test teardown when possible. /// - Tests intentionally do not assert on exact output filenames so they remain resilient to naming changes. /// [Collection("Logging")] public class ProgramIntegrationTests : IDisposable { private readonly TextWriter _origOut; private readonly TextWriter _origErr; private readonly StringWriter _outWriter; private readonly StringWriter _errWriter; private readonly string _root; public ProgramIntegrationTests() { _origOut = Console.Out; _origErr = Console.Error; _outWriter = new StringWriter(); _errWriter = new StringWriter(); Console.SetOut(_outWriter); Console.SetError(_errWriter); _root = Path.Combine(Path.GetTempPath(), "GitConverter.ProgramIntegration", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_root); } public void Dispose() { Console.SetOut(_origOut); Console.SetError(_origErr); _outWriter.Dispose(); _errWriter.Dispose(); try { if (Directory.Exists(_root)) Directory.Delete(_root, true); } catch { } } /// /// Invoke Program.Run(...) via reflection and return the int exit code. /// Robust lookup: Type.GetType -> Assembly.Load -> probe test folder for GitConverter.ConsoleApp*.dll/.exe /// and search parent project build outputs when necessary. /// private int InvokeRun(params string[] args) { const string typeFullName = "GitConverter.ConsoleApp.Program, GitConverter.ConsoleApp"; var tried = new List(); // Fast path: try Type.GetType (works when assembly identity matches and is loaded) var t = Type.GetType(typeFullName); if (t == null) { // Try loading by assembly name (may succeed if assembly is resolvable by name) try { var asm = Assembly.Load("GitConverter.ConsoleApp"); t = asm?.GetType("GitConverter.ConsoleApp.Program"); } catch { /* ignore load errors */ } } // Probe test runtime folder (AppContext.BaseDirectory) if (t == null) { var baseDir = AppContext.BaseDirectory; tried.Add(baseDir); try { foreach (var path in Directory.GetFiles(baseDir, "GitConverter.ConsoleApp*.dll")) { tried.Add(path); try { var a = Assembly.LoadFrom(path); t = a.GetType("GitConverter.ConsoleApp.Program"); if (t != null) break; } catch { /* ignore and continue */ } } if (t == null) { foreach (var path in Directory.GetFiles(baseDir, "GitConverter.ConsoleApp*.exe")) { tried.Add(path); try { var a = Assembly.LoadFrom(path); t = a.GetType("GitConverter.ConsoleApp.Program"); if (t != null) break; } catch { /* ignore and continue */ } } } } catch { /* ignore IO errors */ } } // Search upward for project build outputs (e.g. ../GitConverter.ConsoleApp/bin/Debug/net9.0) if (t == null) { var current = new DirectoryInfo(AppContext.BaseDirectory); for (int depth = 0; depth < 6 && current != null; depth++) { try { var candidateProjectDir = Path.Combine(current.FullName, "GitConverter.ConsoleApp", "bin"); tried.Add(candidateProjectDir); if (Directory.Exists(candidateProjectDir)) { foreach (var cfgDir in Directory.EnumerateDirectories(candidateProjectDir)) { try { var files = Directory.GetFiles(cfgDir, "GitConverter.ConsoleApp*.dll", SearchOption.AllDirectories) .Concat(Directory.GetFiles(cfgDir, "GitConverter.ConsoleApp*.exe", SearchOption.AllDirectories)); foreach (var f in files) { tried.Add(f); try { var a = Assembly.LoadFrom(f); t = a.GetType("GitConverter.ConsoleApp.Program"); if (t != null) break; } catch { /* ignore */ } } if (t != null) break; } catch { /* ignore per-config IO issues */ } } } } catch { /* ignore */ } current = current.Parent; } } if (t == null) { var msg = $"Program type not found - ensure the ConsoleApp project is referenced by the test project. Probed: {string.Join(", ", tried)}"; throw new InvalidOperationException(msg); } var mi = t.GetMethod("Run", BindingFlags.Static | BindingFlags.Public); if (mi == null) throw new InvalidOperationException("Run method not found on Program type. Ensure Program exposes a public static Run(string[])."); var result = mi.Invoke(null, new object[] { args }); return Convert.ToInt32(result); } [Fact(DisplayName = "Program.Run converts provided shapefile archives to all target options (when shapefile archives present)")] public void Program_Run_ShapefileArchives_ConvertToAllTargets_WhenPresent() { var dataFolder = IntegrationTestConstants.TestDataShapefileFolder; if (!Directory.Exists(dataFolder)) return; var samples = Directory.EnumerateFiles(dataFolder, "*", SearchOption.TopDirectoryOnly) .Where(f => f.EndsWith(".7z", StringComparison.OrdinalIgnoreCase) || f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) .ToList(); if (!samples.Any()) return; foreach (var archive in samples) { var archiveName = Path.GetFileName(archive); foreach (var target in IntegrationTestConstants.TargetOptions) { var baseName = Path.GetFileNameWithoutExtension(archiveName); var outFolder = Path.Combine(_root, $"{baseName}_{target}_out"); var tempDir = Path.Combine(_root, $"{baseName}_{target}_tmp"); var logDir = Path.Combine(_root, "logs"); // ensure parent folders exist Directory.CreateDirectory(outFolder); Directory.CreateDirectory(tempDir); Directory.CreateDirectory(logDir); var args = new[] { "gis_convert", archive, target, outFolder, tempDir, logDir }; _outWriter.GetStringBuilder().Clear(); _errWriter.GetStringBuilder().Clear(); var exit = InvokeRun(args); var stdout = _outWriter.ToString(); var stderr = _errWriter.ToString(); var combined = string.Concat(stdout, Environment.NewLine, stderr).Trim(); if (exit == 0) { // Require an output folder and at least one file inside it. Assert.True(Directory.Exists(outFolder), $"Expected output folder '{outFolder}' for '{archiveName}' -> '{target}'. Console: {combined}"); var files = Directory.EnumerateFiles(outFolder, "*", SearchOption.AllDirectories).ToList(); Assert.True(files.Count > 0, $"Expected one or more output files in '{outFolder}' for '{archiveName}' -> '{target}'."); } else { Assert.False(string.IsNullOrWhiteSpace(combined), $"Program.Run exited {exit} for '{archiveName}' -> '{target}' but produced no console output."); } try { if (Directory.Exists(outFolder)) Directory.Delete(outFolder, true); } catch { } try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { } try { if (Directory.Exists(logDir)) Directory.Delete(logDir, true); } catch { } } } } [Fact(DisplayName = "Program.Run converts CSV samples to all target options (when CSV samples present)")] public void Program_Run_CsvFiles_ConvertToAllTargets_WhenPresent() { var dataFolder = IntegrationTestConstants.TestDataCsvFolder; if (!Directory.Exists(dataFolder)) return; var samples = Directory.EnumerateFiles(dataFolder, "*", SearchOption.TopDirectoryOnly) .Where(f => f.EndsWith(".csv", StringComparison.OrdinalIgnoreCase) || f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) || f.EndsWith(".7z", StringComparison.OrdinalIgnoreCase)) .ToList(); if (!samples.Any()) return; foreach (var samplePath in samples) { var sampleName = Path.GetFileName(samplePath); foreach (var target in IntegrationTestConstants.TargetOptions) { var baseName = Path.GetFileNameWithoutExtension(sampleName); var outFolder = Path.Combine(_root, $"{baseName}_{target}_out"); var tempDir = Path.Combine(_root, $"{baseName}_{target}_tmp"); var logDir = Path.Combine(_root, "logs"); Directory.CreateDirectory(outFolder); Directory.CreateDirectory(tempDir); Directory.CreateDirectory(logDir); var args = new[] { "gis_convert", samplePath, target, outFolder, tempDir, logDir }; _outWriter.GetStringBuilder().Clear(); _errWriter.GetStringBuilder().Clear(); var exit = InvokeRun(args); var stdout = _outWriter.ToString(); var stderr = _errWriter.ToString(); var combined = string.Concat(stdout, Environment.NewLine, stderr).Trim(); if (exit == 0) { Assert.True(Directory.Exists(outFolder), $"Expected output folder '{outFolder}' for '{sampleName}' -> '{target}'. Console: {combined}"); var files = Directory.EnumerateFiles(outFolder, "*", SearchOption.AllDirectories).ToList(); Assert.True(files.Count > 0, $"Expected one or more output files in '{outFolder}' for '{sampleName}' -> '{target}'."); } else { Assert.False(string.IsNullOrWhiteSpace(combined), $"Program.Run exited {exit} for '{sampleName}' -> '{target}' but produced no console output."); } try { if (Directory.Exists(outFolder)) Directory.Delete(outFolder, true); } catch { } try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { } try { if (Directory.Exists(logDir)) Directory.Delete(logDir, true); } catch { } } } } [Fact(DisplayName = "Program.Run converts GML samples to all target options (when GML samples present)")] public void Program_Run_GmlFiles_ConvertToAllTargets_WhenPresent() { var dataFolder = IntegrationTestConstants.TestDataGmlFolder; if (!Directory.Exists(dataFolder)) return; var samples = Directory.EnumerateFiles(dataFolder, "*", SearchOption.TopDirectoryOnly) .Where(f => f.EndsWith(".gml", StringComparison.OrdinalIgnoreCase) || f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) || f.EndsWith(".7z", StringComparison.OrdinalIgnoreCase)) .ToList(); if (!samples.Any()) return; foreach (var samplePath in samples) { var sampleName = Path.GetFileName(samplePath); foreach (var target in IntegrationTestConstants.TargetOptions) { var baseName = Path.GetFileNameWithoutExtension(sampleName); var outFolder = Path.Combine(_root, $"{baseName}_{target}_out"); var tempDir = Path.Combine(_root, $"{baseName}_{target}_tmp"); var logDir = Path.Combine(_root, "logs"); Directory.CreateDirectory(outFolder); Directory.CreateDirectory(tempDir); Directory.CreateDirectory(logDir); var args = new[] { "gis_convert", samplePath, target, outFolder, tempDir, logDir }; _outWriter.GetStringBuilder().Clear(); _errWriter.GetStringBuilder().Clear(); var exit = InvokeRun(args); var stdout = _outWriter.ToString(); var stderr = _errWriter.ToString(); var combined = string.Concat(stdout, Environment.NewLine, stderr).Trim(); if (exit == 0) { Assert.True(Directory.Exists(outFolder), $"Expected output folder '{outFolder}' for '{sampleName}' -> '{target}'. Console: {combined}"); var files = Directory.EnumerateFiles(outFolder, "*", SearchOption.AllDirectories).ToList(); Assert.True(files.Count > 0, $"Expected one or more output files in '{outFolder}' for '{sampleName}' -> '{target}'."); } else { Assert.False(string.IsNullOrWhiteSpace(combined), $"Program.Run exited {exit} for '{sampleName}' -> '{target}' but produced no console output."); } try { if (Directory.Exists(outFolder)) Directory.Delete(outFolder, true); } catch { } try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { } try { if (Directory.Exists(logDir)) Directory.Delete(logDir, true); } catch { } } } } } }