Make the console return a non-zero exit code on error, add console app tests, and remove static variables in console deps

This commit is contained in:
Ben Olden-Cooligan 2022-07-02 11:21:21 -07:00
parent dafc26c6b3
commit 425b896cb4
12 changed files with 115 additions and 44 deletions

View File

@ -8,9 +8,9 @@ static class Program
/// The NAPS2.Console.exe main method.
/// </summary>
[STAThread]
static void Main(string[] args)
static int Main(string[] args)
{
// Use reflection to avoid antivirus false positives (yes, really)
typeof(ConsoleEntryPoint).GetMethod("Run").Invoke(null, new object[] { args });
return (int) typeof(ConsoleEntryPoint).GetMethod("Run")!.Invoke(null, new object[] { args })!;
}
}

View File

@ -72,4 +72,10 @@ public static class AppTestHelper
var path = Path.Combine(appData, "NAPS2", "errorlog.txt");
Assert.False(File.Exists(path));
}
public static void AssertErrorLog(string appData)
{
var path = Path.Combine(appData, "NAPS2", "errorlog.txt");
Assert.True(File.Exists(path));
}
}

View File

@ -0,0 +1,53 @@
using NAPS2.Sdk.Tests;
using Xunit;
namespace NAPS2.App.Tests;
public class ConsoleAppTests : ContextualTexts
{
[Fact]
public void ConvertsImportedFile()
{
var importPath = Path.Combine(FolderPath, "in.png");
SharedData.color_image.Save(importPath);
var outputPath = Path.Combine(FolderPath, "out.jpg");
var args = $"-n 0 -i \"{importPath}\" -o \"{outputPath}\"";
var process = AppTestHelper.StartProcess("NAPS2.Console.exe", FolderPath, args);
try
{
Assert.True(process.WaitForExit(2000));
Assert.Equal(0, process.ExitCode);
Assert.Empty(process.StandardOutput.ReadToEnd());
AppTestHelper.AssertNoErrorLog(FolderPath);
Assert.True(File.Exists(outputPath));
}
finally
{
AppTestHelper.Cleanup(process);
}
}
[Fact]
public void NonZeroExitCodeForError()
{
var importPath = Path.Combine(FolderPath, "doesnotexist.png");
var outputPath = Path.Combine(FolderPath, "out.jpg");
var args = $"-n 0 -i \"{importPath}\" -o \"{outputPath}\"";
var process = AppTestHelper.StartProcess("NAPS2.Console.exe", FolderPath, args);
try
{
Assert.True(process.WaitForExit(2000));
var stdout = process.StandardOutput.ReadToEnd();
Assert.NotEqual(0, process.ExitCode);
Assert.NotEmpty(stdout);
AppTestHelper.AssertErrorLog(FolderPath);
Assert.False(File.Exists(outputPath));
}
finally
{
AppTestHelper.Cleanup(process);
}
}
}

View File

@ -22,5 +22,4 @@ public class WinFormsAppTests : ContextualTexts
AppTestHelper.Cleanup(process);
}
}
}

View File

@ -86,7 +86,6 @@ public class AutomatedScanning
}
_placeholders = Placeholders.All.WithDate(DateTime.Now);
ConsoleOverwritePrompt.ForceOverwrite = _options.ForceOverwrite;
if (_options.Install != null)
{
@ -266,8 +265,6 @@ public class AutomatedScanning
{
OutputVerbose(ConsoleResources.Importing);
ConsolePdfPasswordProvider.PasswordToProvide = _options.ImportPassword;
var filePaths = _options.ImportPath.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
int i = 0;
foreach (var filePath in filePaths)
@ -292,6 +289,7 @@ public class AutomatedScanning
{
Log.ErrorException(string.Format(ConsoleResources.ErrorImporting, filePath), ex);
_errorOutput.DisplayError(string.Format(ConsoleResources.ErrorImporting, filePath));
// TODO: Should we really continue?
continue;
}
OutputVerbose(ConsoleResources.ImportedFile, i, filePaths.Length);

View File

@ -9,27 +9,27 @@ public class AutomatedScanningOptions
[Option('o', "output", HelpText = "The name and path of the file to save." +
" The extension determines the output type (e.g. .pdf for a PDF file, .jpg for a JPEG)." +
" Placeholders can be used (e.g. $(YYYY)-$(MM)-$(DD) for the date, $(hh)_$(mm)_$(ss) for the time, $(nnnn) for an auto-incrementing number).")]
public string OutputPath { get; set; }
public string? OutputPath { get; set; }
[Option('a', "autosave", HelpText = "Use the Auto Save settings from the selected profile." +
" Only works if the profile has Auto Save enabled.")]
public bool AutoSave { get; set; }
[Option("install", HelpText = "Use this option to download and install optional components (e.g. \"ocr-eng\", \"generic-import\").")]
public string Install { get; set; }
public string? Install { get; set; }
[Option('p', "profile", HelpText = "The name of the profile to use for scanning." +
" If not specified, the most-recently-used profile from the GUI is selected.")]
public string ProfileName { get; set; }
public string? ProfileName { get; set; }
[Option('i', "import", HelpText = "The name and path of one or more pdf/image files to import." +
" Imported files are prepended to the output in the order they are specified." +
" Multiple files are separated by a semicolon (\";\")." +
" Slice notation can be used to only import some pages (e.g. \"[0]\" for the first page or \"[:2]\" for the first two pages).")]
public string ImportPath { get; set; }
public string? ImportPath { get; set; }
[Option("importpassword", HelpText = "The password to use to import one or more encrypted PDF files.")]
public string ImportPassword { get; set; }
public string? ImportPassword { get; set; }
[Option("progress", HelpText = "Display a graphical window for scanning progress.")]
public bool Progress { get; set; }
@ -91,16 +91,16 @@ public class AutomatedScanningOptions
#region PDF Options
[Option("pdftitle", HelpText = "The title for generated PDF metadata.")]
public string PdfTitle { get; set; }
public string? PdfTitle { get; set; }
[Option("pdfauthor", HelpText = "The author for generated PDF metadata.")]
public string PdfAuthor { get; set; }
public string? PdfAuthor { get; set; }
[Option("pdfsubject", HelpText = "The subject for generated PDF metadata.")]
public string PdfSubject { get; set; }
public string? PdfSubject { get; set; }
[Option("pdfkeywords", HelpText = "The keywords for generated PDF metadata.")]
public string PdfKeywords { get; set; }
public string? PdfKeywords { get; set; }
[Option("usesavedmetadata", HelpText = "Use the metadata (title, author, subject, keywords) configured in the GUI, if any, for the generated PDF.")]
public bool UseSavedMetadata { get; set; }
@ -134,29 +134,29 @@ public class AutomatedScanningOptions
[Option('e', "email", HelpText = "The name of the file to attach to an email." +
" The extension determines the output type (e.g. .pdf for a PDF file, .jpg for a JPEG).")]
//" You can use \"<date>\" and/or \"<time>\" to insert the date/time of the scan.")]
public string EmailFileName { get; set; }
public string? EmailFileName { get; set; }
[Option("subject", HelpText = "The email message's subject." +
//" You can use \"<date>\" and/or \"<time>\" to insert the date/time of the scan." +
" Requires -e/--email.")]
public string EmailSubject { get; set; }
public string? EmailSubject { get; set; }
[Option("body", HelpText = "The email message's body text." +
//" You can use \"<date>\" and/or \"<time>\" to insert the date/time of the scan." +
" Requires -e/--email.")]
public string EmailBody { get; set; }
public string? EmailBody { get; set; }
[Option("to", HelpText = "A comma-separated list of email addresses of the recipients." +
" Requires -e/--email.")]
public string EmailTo { get; set; }
public string? EmailTo { get; set; }
[Option("cc", HelpText = "A comma-separated list of email addresses of the recipients." +
" Requires -e/--email.")]
public string EmailCc { get; set; }
public string? EmailCc { get; set; }
[Option("bcc", HelpText = "A comma-separated list of email addresses of the recipients." +
" Requires -e/--email.")]
public string EmailBcc { get; set; }
public string? EmailBcc { get; set; }
[Option("autosend", HelpText = "Actually send the email immediately after scanning completes without prompting the user for changes." +
" However, this may prompt the user to login. To avoid that, use --silentsend." +
@ -178,7 +178,7 @@ public class AutomatedScanningOptions
public int JpegQuality { get; set; }
[Option("tiffcomp", HelpText = "The compression to use for TIFF files. Possible values: auto, lzw, ccitt4, none")]
public string TiffComp { get; set; }
public string? TiffComp { get; set; }
#endregion
}

View File

@ -9,8 +9,11 @@ public class ConsoleErrorOutput : ErrorOutput
_output = output;
}
public bool HasError { get; private set; }
public override void DisplayError(string errorMessage)
{
HasError = true;
_output.Writer.WriteLine(errorMessage);
}

View File

@ -5,18 +5,18 @@ namespace NAPS2.Automation;
public class ConsoleOverwritePrompt : IOverwritePrompt
{
public static bool ForceOverwrite { get; set; }
private readonly AutomatedScanningOptions _options;
private readonly ErrorOutput _errorOutput;
public ConsoleOverwritePrompt(ErrorOutput errorOutput)
public ConsoleOverwritePrompt(AutomatedScanningOptions options, ErrorOutput errorOutput)
{
_options = options;
_errorOutput = errorOutput;
}
public OverwriteResponse ConfirmOverwrite(string path)
{
if (ForceOverwrite)
if (_options.ForceOverwrite)
{
return OverwriteResponse.Yes;
}

View File

@ -5,24 +5,24 @@ namespace NAPS2.Automation;
public class ConsolePdfPasswordProvider : IPdfPasswordProvider
{
private readonly AutomatedScanningOptions _options;
private readonly ErrorOutput _errorOutput;
public ConsolePdfPasswordProvider(ErrorOutput errorOutput)
public ConsolePdfPasswordProvider(AutomatedScanningOptions options, ErrorOutput errorOutput)
{
_options = options;
_errorOutput = errorOutput;
}
public bool ProvidePassword(string fileName, int attemptCount, out string password)
{
password = PasswordToProvide ?? "";
password = _options.ImportPassword ?? "";
if (attemptCount > 0)
{
_errorOutput.DisplayError(PasswordToProvide == null
_errorOutput.DisplayError(_options.ImportPassword == null
? ConsoleResources.ImportErrorNoPassword : ConsoleResources.ImportErrorWrongPassword);
return false;
}
return true;
}
public static string PasswordToProvide { get; set; }
}

View File

@ -3,7 +3,6 @@ using NAPS2.Automation;
using NAPS2.Modules;
using NAPS2.Remoting.Worker;
using Ninject;
using Ninject.Parameters;
namespace NAPS2.EntryPoints;
@ -12,25 +11,28 @@ namespace NAPS2.EntryPoints;
/// </summary>
public static class ConsoleEntryPoint
{
public static void Run(string[] args)
public static int Run(string[] args)
{
// Initialize Ninject (the DI framework)
var kernel = new StandardKernel(new CommonModule(), new ConsoleModule(), new RecoveryModule(), new ContextModule());
Paths.ClearTemp();
// Parse the command-line arguments (and display help text if appropriate)
var options = Parser.Default.ParseArguments<AutomatedScanningOptions>(args).Value;
if (options == null)
{
return;
return 0;
}
// Initialize Ninject (the DI framework)
var kernel = new StandardKernel(new CommonModule(), new ConsoleModule(options), new RecoveryModule(),
new ContextModule());
Paths.ClearTemp();
// Start a pending worker process
kernel.Get<IWorkerFactory>().Init();
// Run the scan automation logic
var scanning = kernel.Get<AutomatedScanning>(new ConstructorArgument("options", options));
var scanning = kernel.Get<AutomatedScanning>();
scanning.Execute().Wait();
return ((ConsoleErrorOutput) kernel.Get<ErrorOutput>()).HasError ? 1 : 0;
}
}

View File

@ -9,10 +9,19 @@ namespace NAPS2.Modules;
public class ConsoleModule : NinjectModule
{
private readonly AutomatedScanningOptions _options;
public ConsoleModule(AutomatedScanningOptions options)
{
_options = options;
}
public override void Load()
{
Bind<AutomatedScanningOptions>().ToConstant(_options);
Bind<IPdfPasswordProvider>().To<ConsolePdfPasswordProvider>();
Bind<ErrorOutput>().To<ConsoleErrorOutput>();
Bind<ErrorOutput>().To<ConsoleErrorOutput>().InSingletonScope();
Bind<IOverwritePrompt>().To<ConsoleOverwritePrompt>();
Bind<OperationProgress>().To<ConsoleOperationProgress>();
Bind<IComponentInstallPrompt>().To<ConsoleComponentInstallPrompt>();

View File

@ -29,9 +29,9 @@ public class CommandLineIntegrationTests : ContextualTexts
private async Task RunCommand(AutomatedScanningOptions options, params Bitmap[] imagesToScan)
{
var scanDriverFactory = new ScanDriverFactoryBuilder().WithScannedImages(imagesToScan).Build();
var kernel = new StandardKernel(new CommonModule(), new ConsoleModule(),
var kernel = new StandardKernel(new CommonModule(), new ConsoleModule(options),
new TestModule(ScanningContext, ImageContext, scanDriverFactory, _testOutputHelper, FolderPath));
var automatedScanning = kernel.Get<AutomatedScanning>(new ConstructorArgument("options", options));
var automatedScanning = kernel.Get<AutomatedScanning>();
await automatedScanning.Execute();
}
@ -162,7 +162,8 @@ public class CommandLineIntegrationTests : ContextualTexts
_folderPath)).InSingletonScope();
string recoveryFolderPath = Path.Combine(_folderPath, "recovery");
var recoveryStorageManager = RecoveryStorageManager.CreateFolder(recoveryFolderPath);
var recoveryStorageManager =
RecoveryStorageManager.CreateFolder(recoveryFolderPath, Kernel.Get<UiImageList>());
var fileStorageManager = new FileStorageManager(recoveryFolderPath);
Kernel.Bind<RecoveryStorageManager>().ToConstant(recoveryStorageManager);
Kernel.Bind<FileStorageManager>().ToConstant(fileStorageManager);