using System.Reflection; namespace GitConverter.TestsApp.ConsoleApp { /// /// Basic CLI tests for Program.Run: help, unknown-option behavior and tolerant unquoted-input repair. /// /// /// Responsibilities /// - Validate basic CLI behavior of the ConsoleApp entry point without invoking real conversions. /// - Run Program.Run(...) via reflection so test process is not terminated by Environment.Exit. /// /// Test strategy /// - Capture Console.Out and Console.Error to assert on user-facing messages. /// - Use reflection-based InvokeRun to locate Program.Run in the built ConsoleApp assembly. The helper /// tries multiple resolution strategies so tests work both inside the IDE and in build/CI outputs. /// - Tests are intentionally lightweight: they assert presence of usage/help text, unknown-option messages, /// and that the tolerant unquoted-input repair logic does not treat path fragments as the target option. /// /// Notes /// - These tests do not require sample data on disk. They focus on CLI parsing, messaging and argument repair logic. /// - When adding additional CLI features extend tests to verify new messages and exit codes. /// [Collection("Logging")] public class ProgramTests : IDisposable { private readonly TextWriter _originalOut; private readonly TextWriter _originalErr; private readonly StringWriter _outWriter; private readonly StringWriter _errWriter; public ProgramTests() { _originalOut = Console.Out; _originalErr = Console.Error; _outWriter = new StringWriter(); _errWriter = new StringWriter(); Console.SetOut(_outWriter); Console.SetError(_errWriter); } public void Dispose() { Console.SetOut(_originalOut); Console.SetError(_originalErr); _outWriter.Dispose(); _errWriter.Dispose(); } /// /// 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(); var t = Type.GetType(typeFullName); if (t == null) { try { var asm = Assembly.Load("GitConverter.ConsoleApp"); t = asm?.GetType("GitConverter.ConsoleApp.Program"); } catch { /* ignore */ } } if (t == null) { var baseDir = AppContext.BaseDirectory; tried.Add(baseDir); try { foreach (var path in Directory.GetFiles(baseDir, "GitConverter.ConsoleApp*.dll").Concat(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 */ } } } catch { /* ignore */ } } 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 */ } } } } 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 = "No args prints help")] public void Run_NoArgs_PrintsHelp() { var exit = InvokeRun(Array.Empty()); var outText = _outWriter.ToString(); Assert.Contains("GIS Converter CLI Tool", outText, StringComparison.OrdinalIgnoreCase); Assert.Contains("Usage", outText, StringComparison.OrdinalIgnoreCase); // Program.Run returns AppError (1) when showing help / no args Assert.Equal(1, exit); } [Fact(DisplayName = "Unknown conversion option prints unknown message")] public void Run_GisConvert_UnknownOption_PrintsUnknownOption() { var args = new[] { "gis_convert", "dummy.input", "zzzz_nonexistent", Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")), Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")), "unused" }; var exit = InvokeRun(args); var outText = _outWriter.ToString(); var containsUnknown = outText.Contains("Unable to auto-detect converter from input", StringComparison.OrdinalIgnoreCase) || outText.Contains("Detector reason", StringComparison.OrdinalIgnoreCase) || outText.Contains("Please use 'help'", StringComparison.OrdinalIgnoreCase); var containsUsage = outText.Contains("Usage", StringComparison.OrdinalIgnoreCase); Assert.True(containsUnknown || containsUsage, $"Expected either 'Unknown target format option', 'Unknown format option' or usage output. Actual output:\n{outText}"); Assert.Equal(1, exit); } [Fact(DisplayName = "Unquoted input path is repaired and not treated as target option")] public void Run_GisConvert_UnquotedInput_Repaired() { // Simulate an input path tokenized by the shell (user forgot to quote). // Place a known supported option token ("Shapefile") after the path fragments. var outDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); Directory.CreateDirectory(outDir); Directory.CreateDirectory(tempDir); var args = new[] { "gis_convert", // split input path across tokens (no quotes) @"D:\GisConverter\Tests\Shapefile\Input\שכבות", "מידע", "(Arc", "View).7z", "Shapefile", // recognized target outDir, tempDir, "unused" }; _outWriter.GetStringBuilder().Clear(); _errWriter.GetStringBuilder().Clear(); var exit = InvokeRun(args); var outText = _outWriter.ToString() + Environment.NewLine + _errWriter.ToString(); // The program should not complain about unknown target option (repair happened). Assert.DoesNotContain("Unknown target format option", outText, StringComparison.OrdinalIgnoreCase); // Clean up try { if (Directory.Exists(outDir)) Directory.Delete(outDir, true); } catch { } try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { } } } }