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 { }
}
}
}
}
}