using Amazon.Lambda.TestTool.Runtime;
using Amazon.Lambda.TestTool.SampleRequests;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
namespace Amazon.Lambda.TestTool
{
public class TestToolStartup
{
public class RunConfiguration
{
public enum RunMode { Normal, Test };
///
/// If this is set to Test then that disables any interactive activity or any calls to Environment.Exit which wouldn't work well during a test run.
///
public RunMode Mode { get; set; } = RunMode.Normal;
///
/// Allows you to capture the output for tests to example instead of just writing to the console windows.
///
public TextWriter OutputWriter { get; set; } = Console.Out;
}
public static void Startup(string productName, Action uiStartup, string[] args)
{
Startup(productName, uiStartup, args, new RunConfiguration());
}
public static void Startup(string productName, Action uiStartup, string[] args, RunConfiguration runConfiguration)
{
try
{
Utils.PrintToolTitle(productName);
var commandOptions = CommandLineOptions.Parse(args);
if (commandOptions.ShowHelp)
{
CommandLineOptions.PrintUsage();
return;
}
var localLambdaOptions = new LocalLambdaOptions()
{
Host = commandOptions.Host,
Port = commandOptions.Port
};
var lambdaAssemblyDirectory = commandOptions.Path ?? Directory.GetCurrentDirectory();
#if NETCOREAPP3_1
var targetFramework = "netcoreapp3.1";
#elif NET5_0
var targetFramework = "net5.0";
#elif NET6_0
var targetFramework = "net6.0";
#elif NET7_0
var targetFramework = "net7.0";
#endif
// Check to see if running in debug mode from this project's directory which means the test tool is being debugged.
// To make debugging easier pick one of the test Lambda projects.
if (lambdaAssemblyDirectory.EndsWith("Amazon.Lambda.TestTool.WebTester21"))
{
lambdaAssemblyDirectory = Path.Combine(lambdaAssemblyDirectory, $"../../tests/LambdaFunctions/netcore21/S3EventFunction/bin/Debug/{targetFramework}");
}
else if (lambdaAssemblyDirectory.EndsWith("Amazon.Lambda.TestTool.WebTester31"))
{
lambdaAssemblyDirectory = Path.Combine(lambdaAssemblyDirectory, $"../../tests/LambdaFunctions/netcore31/S3EventFunction/bin/Debug/{targetFramework}");
}
// If running in the project directory select the build directory so the deps.json file can be found.
else if (Utils.IsProjectDirectory(lambdaAssemblyDirectory))
{
lambdaAssemblyDirectory = Path.Combine(lambdaAssemblyDirectory, $"bin/Debug/{targetFramework}");
}
lambdaAssemblyDirectory = Utils.SearchLatestCompilationDirectory(lambdaAssemblyDirectory);
localLambdaOptions.LambdaRuntime = LocalLambdaRuntime.Initialize(lambdaAssemblyDirectory);
runConfiguration.OutputWriter.WriteLine($"Loaded local Lambda runtime from project output {lambdaAssemblyDirectory}");
if (commandOptions.NoUI)
{
ExecuteWithNoUi(localLambdaOptions, commandOptions, lambdaAssemblyDirectory, runConfiguration);
}
else
{
// Look for aws-lambda-tools-defaults.json or other config files.
localLambdaOptions.LambdaConfigFiles = Utils.SearchForConfigFiles(lambdaAssemblyDirectory);
// Start the test tool web server.
uiStartup(localLambdaOptions, !commandOptions.NoLaunchWindow);
}
}
catch (CommandLineParseException e)
{
runConfiguration.OutputWriter.WriteLine($"Invalid command line arguments: {e.Message}");
runConfiguration.OutputWriter.WriteLine("Use the --help option to learn about the possible command line arguments");
if (runConfiguration.Mode == RunConfiguration.RunMode.Normal)
{
if (Debugger.IsAttached)
{
Console.WriteLine("Press any key to exit");
Console.ReadKey();
}
System.Environment.Exit(-1);
}
}
catch (Exception e)
{
runConfiguration.OutputWriter.WriteLine($"Unknown error occurred causing process exit: {e.Message}");
runConfiguration.OutputWriter.WriteLine(e.StackTrace);
if (runConfiguration.Mode == RunConfiguration.RunMode.Normal)
{
if (Debugger.IsAttached)
{
Console.WriteLine("Press any key to exit");
Console.ReadKey();
}
System.Environment.Exit(-2);
}
}
}
public static void ExecuteWithNoUi(LocalLambdaOptions localLambdaOptions, CommandLineOptions commandOptions, string lambdaAssemblyDirectory, RunConfiguration runConfiguration)
{
runConfiguration.OutputWriter.WriteLine("Executing Lambda function without web interface");
var lambdaProjectDirectory = Utils.FindLambdaProjectDirectory(lambdaAssemblyDirectory);
string configFile = DetermineConfigFile(commandOptions, lambdaAssemblyDirectory: lambdaAssemblyDirectory, lambdaProjectDirectory: lambdaProjectDirectory);
LambdaConfigInfo configInfo = LoadLambdaConfigInfo(configFile, commandOptions, lambdaAssemblyDirectory: lambdaAssemblyDirectory, lambdaProjectDirectory: lambdaProjectDirectory, runConfiguration);
LambdaFunction lambdaFunction = LoadLambdaFunction(configInfo, localLambdaOptions, commandOptions, lambdaAssemblyDirectory: lambdaAssemblyDirectory, lambdaProjectDirectory: lambdaProjectDirectory, runConfiguration);
string payload = DeterminePayload(localLambdaOptions, commandOptions, lambdaAssemblyDirectory: lambdaAssemblyDirectory, lambdaProjectDirectory: lambdaProjectDirectory, runConfiguration);
var awsProfile = commandOptions.AWSProfile ?? configInfo.AWSProfile;
if (!string.IsNullOrEmpty(awsProfile))
{
if (new Amazon.Runtime.CredentialManagement.CredentialProfileStoreChain().TryGetProfile(awsProfile, out _))
{
runConfiguration.OutputWriter.WriteLine($"... Setting AWS_PROFILE environment variable to {awsProfile}.");
}
else
{
runConfiguration.OutputWriter.WriteLine($"... Warning: Profile {awsProfile} not found in the aws credential store.");
awsProfile = null;
}
}
else
{
runConfiguration.OutputWriter.WriteLine("... No profile choosen for AWS credentials. The --profile switch can be used to configure an AWS profile.");
}
var awsRegion = commandOptions.AWSRegion ?? configInfo.AWSRegion;
if (!string.IsNullOrEmpty(awsRegion))
{
runConfiguration.OutputWriter.WriteLine($"... Setting AWS_REGION environment variable to {awsRegion}.");
}
else
{
runConfiguration.OutputWriter.WriteLine("... No default AWS region configured. The --region switch can be used to configure an AWS Region.");
}
// Create the execution request that will be sent into the LocalLambdaRuntime.
var request = new ExecutionRequest()
{
AWSProfile = awsProfile,
AWSRegion = awsRegion,
Payload = payload,
Function = lambdaFunction
};
ExecuteRequest(request, localLambdaOptions, runConfiguration);
if (runConfiguration.Mode == RunConfiguration.RunMode.Normal && commandOptions.PauseExit)
{
Console.WriteLine("Press any key to exit");
Console.ReadKey();
}
}
private static string DetermineConfigFile(CommandLineOptions commandOptions, string lambdaAssemblyDirectory, string lambdaProjectDirectory)
{
string configFile = null;
if (string.IsNullOrEmpty(commandOptions.ConfigFile))
{
configFile = Utils.SearchForConfigFiles(lambdaAssemblyDirectory).FirstOrDefault(x => string.Equals(Utils.DEFAULT_CONFIG_FILE, Path.GetFileName(x), StringComparison.OrdinalIgnoreCase));
}
else if (Path.IsPathRooted(commandOptions.ConfigFile))
{
configFile = commandOptions.ConfigFile;
}
else if (File.Exists(Path.Combine(lambdaAssemblyDirectory, commandOptions.ConfigFile)))
{
configFile = Path.Combine(lambdaAssemblyDirectory, commandOptions.ConfigFile);
}
else if (lambdaProjectDirectory != null && File.Exists(Path.Combine(lambdaProjectDirectory, commandOptions.ConfigFile)))
{
configFile = Path.Combine(lambdaProjectDirectory, commandOptions.ConfigFile);
}
return configFile;
}
private static LambdaConfigInfo LoadLambdaConfigInfo(string configFile, CommandLineOptions commandOptions, string lambdaAssemblyDirectory, string lambdaProjectDirectory, RunConfiguration runConfiguration)
{
LambdaConfigInfo configInfo;
if (configFile != null)
{
runConfiguration.OutputWriter.WriteLine($"... Using config file {configFile}");
configInfo = LambdaDefaultsConfigFileParser.LoadFromFile(configFile);
}
else
{
// If no config files or function handler are set then we don't know what code to call and must give up.
if (string.IsNullOrEmpty(commandOptions.FunctionHandler))
{
throw new CommandLineParseException("No config file or function handler specified to test tool is unable to identify the Lambda code to execute.");
}
// No config files were found so create a temporary config file and use the function handler value that was set on the command line.
configInfo = LambdaDefaultsConfigFileParser.LoadFromFile(new LambdaConfigFile
{
FunctionHandler = commandOptions.FunctionHandler,
ConfigFileLocation = lambdaProjectDirectory ?? lambdaAssemblyDirectory
});
}
return configInfo;
}
private static LambdaFunction LoadLambdaFunction(LambdaConfigInfo configInfo, LocalLambdaOptions localLambdaOptions, CommandLineOptions commandOptions, string lambdaAssemblyDirectory, string lambdaProjectDirectory, RunConfiguration runConfiguration)
{
// If no function handler was explicitly set and there is only one function defined in the config file then assume the user wants to debug that function.
var functionHandler = commandOptions.FunctionHandler;
if (string.IsNullOrEmpty(commandOptions.FunctionHandler))
{
if (configInfo.FunctionInfos.Count == 1)
{
functionHandler = configInfo.FunctionInfos[0].Handler;
}
else
{
throw new CommandLineParseException("Project has more then one Lambda function defined. Use the --function-handler switch to identify the Lambda code to execute.");
}
}
LambdaFunction lambdaFunction;
if (!localLambdaOptions.TryLoadLambdaFuntion(configInfo, functionHandler, out lambdaFunction))
{
// The user has explicitly set a function handler value that is not in the config file or CloudFormation template.
// To support users testing add hoc methods create a temporary config object using explicit function handler value.
runConfiguration.OutputWriter.WriteLine($"... Info: function handler {functionHandler} is not defined in config file.");
var temporaryConfigInfo = LambdaDefaultsConfigFileParser.LoadFromFile(new LambdaConfigFile
{
FunctionHandler = functionHandler,
ConfigFileLocation = Utils.FindLambdaProjectDirectory(lambdaAssemblyDirectory) ?? lambdaAssemblyDirectory
});
temporaryConfigInfo.AWSProfile = configInfo.AWSProfile;
temporaryConfigInfo.AWSRegion = configInfo.AWSRegion;
configInfo = temporaryConfigInfo;
lambdaFunction = localLambdaOptions.LoadLambdaFuntion(configInfo, functionHandler);
}
runConfiguration.OutputWriter.WriteLine($"... Using function handler {functionHandler}");
return lambdaFunction;
}
private static string DeterminePayload(LocalLambdaOptions localLambdaOptions, CommandLineOptions commandOptions, string lambdaAssemblyDirectory, string lambdaProjectDirectory, RunConfiguration runConfiguration)
{
var payload = commandOptions.Payload;
bool payloadFileFound = false;
if (!string.IsNullOrEmpty(payload))
{
if (Path.IsPathFullyQualified(payload) && File.Exists(payload))
{
runConfiguration.OutputWriter.WriteLine($"... Using payload with from the file {payload}");
payload = File.ReadAllText(payload);
payloadFileFound = true;
}
else
{
// Look to see if the payload value is a file in
// * Directory with user Lambda assemblies.
// * Lambda project directory
// * Properties directory under the project directory. This is to make it easy to reconcile from the launchSettings.json file.
// * Is a saved sample request from the web interface
var possiblePaths = new[]
{
Path.Combine(lambdaAssemblyDirectory, payload),
Path.Combine(lambdaProjectDirectory, payload),
Path.Combine(lambdaProjectDirectory, "Properties", payload),
Path.Combine(localLambdaOptions.GetPreferenceDirectory(false), new SampleRequestManager(localLambdaOptions.GetPreferenceDirectory(false)).GetSaveRequestRelativePath(payload))
};
foreach (var possiblePath in possiblePaths)
{
if (File.Exists(possiblePath))
{
runConfiguration.OutputWriter.WriteLine($"... Using payload with from the file {Path.GetFullPath(possiblePath)}");
payload = File.ReadAllText(possiblePath);
payloadFileFound = true;
break;
}
}
}
}
if (!payloadFileFound)
{
if (!string.IsNullOrEmpty(payload))
{
runConfiguration.OutputWriter.WriteLine($"... Using payload with the value {payload}");
}
else
{
runConfiguration.OutputWriter.WriteLine("... No payload configured. If a payload is required set the --payload switch to a file path or a JSON document.");
}
}
return payload;
}
private static void ExecuteRequest(ExecutionRequest request, LocalLambdaOptions localLambdaOptions, RunConfiguration runConfiguration)
{
try
{
var response = localLambdaOptions.LambdaRuntime.ExecuteLambdaFunctionAsync(request).GetAwaiter().GetResult();
runConfiguration.OutputWriter.WriteLine("Captured Log information:");
runConfiguration.OutputWriter.WriteLine(response.Logs);
if (response.IsSuccess)
{
runConfiguration.OutputWriter.WriteLine("Request executed successfully");
runConfiguration.OutputWriter.WriteLine("Response:");
runConfiguration.OutputWriter.WriteLine(response.Response);
}
else
{
runConfiguration.OutputWriter.WriteLine("Request failed to execute");
runConfiguration.OutputWriter.WriteLine($"Error:");
runConfiguration.OutputWriter.WriteLine(response.Error);
}
}
catch (Exception e)
{
runConfiguration.OutputWriter.WriteLine("Unknown error occurred in the Lambda test tool while executing request.");
runConfiguration.OutputWriter.WriteLine($"Error Message: {e.Message}");
runConfiguration.OutputWriter.WriteLine(e.StackTrace);
}
}
}
}