using System.Reflection; using GitConverter.TestsApp.TestSupport; namespace GitConverter.TestsApp.ConsoleApp { /// /// Integration tests that exercise the console Program.Run(...) end-to-end. /// /// /// - Executes the ConsoleApp entrypoint as a regular method call (via ) so tests are not /// terminated by Environment.Exit. This avoids spawning a separate process and simplifies CI integration. /// - Tests are defensive: they skip when required sample artifacts are missing to keep CI runs non-fatal when private /// samples are not available. /// - Each test creates isolated temporary output, temp and log folders under a unique test root to avoid collisions. /// - StdOut and StdErr are captured and restored for each test to allow assertions on console output. /// - Tests assert either successful output files in the output folder (exit code 0) or presence of diagnostic console /// output when the command fails (non-zero exit codes). /// [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; /// /// Test constructor — captures console output and creates an isolated temp root. /// /// /// - Redirects and to in-memory buffers. /// - Creates a unique temp root under the OS temp folder; subfolders for each test case are created beneath it. /// - Any long-lived resources or file handles should be released in . /// 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); } /// /// Restores console streams and attempts best-effort cleanup of temporary files. /// /// /// - Restores original console streams to avoid interfering with other tests. /// - Disposes in-memory writers and deletes the temporary root directory if possible. /// - Cleanup is best-effort; exceptions during deletion are swallowed to avoid failing test teardown. /// public void Dispose() { Console.SetOut(_origOut); Console.SetError(_origErr); _outWriter.Dispose(); _errWriter.Dispose(); try { if (Directory.Exists(_root)) Directory.Delete(_root, true); } catch { } } /// /// Invoke the ConsoleApp program entrypoint and return the exit code. /// /// Arguments forwarded to the CLI (first token is the command name). /// Integer exit code returned by Program.Run. /// /// - Tests call this helper instead of calling Environment.Exit to keep the test runner alive. /// - The method calls GitConverter.ConsoleApp.Program.Run(...) directly; if the CLI implementation /// changes to call Environment.Exit unconditionally, tests will need to be adapted to invoke a /// separate process or to use a different invocation strategy. /// - Captured stdout/stderr buffers are available to callers after this returns. /// private int InvokeRun(params string[] args) { return GitConverter.ConsoleApp.Program.Run(args); } /// /// Integration test: run converters against CSV samples found in test data. /// /// /// - Skips when the CSV test data folder is missing or contains no samples. /// - For each sample and target option this test creates isolated out/temp/log folders and asserts on the result. /// - Tests perform best-effort cleanup of created folders. /// [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 { } } } } /// /// Integration test: run converters against GML samples found in test data. /// /// /// - Skips when the GML test data folder is missing or contains no samples. /// - For each sample and target option this test creates isolated out/temp/log folders and asserts on the result. /// - Tests perform best-effort cleanup of created folders. /// [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 { } } } } /// /// Integration test: run converters against shapefile archives found in test data. /// /// /// - Skips when the shapefile test data folder is missing or contains no archives. /// - For each archive and target option this test creates isolated out/temp/log folders, invokes the CLI /// and asserts either successful output files (exit code 0) or non-empty diagnostic console output. /// - Cleans up created folders per iteration to avoid accumulating artifacts on the host. /// [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, "EsriJson", 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 { } } } } } }