blob: eb57fd1dcffcd13c0521f02442ba92e02cc9b365 [file] [log] [blame]
/*
* Copyright (c) 2016, The OpenThread Authors.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holder nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace otTestRunner
{
class Program
{
struct TestResults
{
public bool Pass;
public List<string> Output;
public string Error;
}
static string EscapeJson(string data)
{
return data.Replace("\\", "\\\\").Replace("\"", "\\\"");
}
/// <summary>
/// Executes an exe with the given args and captures the output
/// </summary>
static async Task<TestResults> ExecuteAsync(string name, string args = null, int timeoutMilliseconds = -1, int instanceIndex = -1)
{
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.WindowStyle = ProcessWindowStyle.Hidden;
startInfo.CreateNoWindow = true;
startInfo.UseShellExecute = false;
startInfo.RedirectStandardError = true;
startInfo.RedirectStandardOutput = true;
startInfo.FileName = name;
startInfo.Arguments = args;
startInfo.EnvironmentVariables["NODE_TYPE"] = "win-sim";
if (instanceIndex != -1)
{
startInfo.EnvironmentVariables["INSTANCE"] = instanceIndex.ToString();
}
TestResults Results = new TestResults();
Results.Pass = false;
Results.Output = new List<string>();
Results.Error = null;
var Errors = new List<string>();
Results.Output.Add(string.Format("> set NODE_TYPE=win-sim"));
Results.Output.Add(string.Format("> {0} {1}", name, args));
Results.Output.Add("----------------------------------------------------------------------");
try
{
// Execute process
using (Process process = Process.Start(startInfo))
{
process.OutputDataReceived +=
(object sender, DataReceivedEventArgs e) => {
if (e.Data != null && e.Data.Length > 0)
lock (Results.Output) { Results.Output.Add(e.Data); }
};
process.ErrorDataReceived +=
(object sender, DataReceivedEventArgs e) => {
if (e.Data != null && e.Data.Length > 0)
lock (Results.Output) { Errors.Add(e.Data); }
};
process.BeginErrorReadLine();
process.BeginOutputReadLine();
#if DEBUG
Console.WriteLine("Starting {0} {1}", name, args);
#endif
// Wait for process to complete
await Task.Run(
() => {
if (timeoutMilliseconds == -1)
process.WaitForExit();
else if (!process.WaitForExit(timeoutMilliseconds))
{
process.Kill();
Results.Output.Add(string.Format("Killed {0} on execution timeout!", name));
}
});
// Wait a bit for any output to collect
await Task.Delay(1000);
process.CancelOutputRead();
process.CancelErrorRead();
Results.Output.AddRange(Errors);
Results.Pass = Errors.Count > 0 && Errors[Errors.Count - 1] == "OK";
if (!Results.Pass) Results.Error = EscapeJson(string.Join("\\r\\n", Errors));
// Make sure the process is killed
try { process.Kill(); } catch (Exception) { }
#if DEBUG
Console.WriteLine("Completed {0} {1}", name, args);
#endif
}
}
catch (Exception e)
{
Results.Output.Add("Encountered exception: " + e.Message);
Results.Output.Add(e.StackTrace);
}
return Results;
}
static bool VerboseOutput = false;
static int RetiresOnFailure = 0;
static bool AppVeyorMode = false;
static string AppVeyorApiUrl = null;
static string ResultsFolder = "Results_" + DateTime.Now.ToString("yyyyMMdd_HH.mm.ss");
static void UploadAppVeyorTestResult(string name, bool passed, long durationMS, string error = null)
{
if (AppVeyorApiUrl == null) return;
string jsonData = null;
try
{
var request = (HttpWebRequest)WebRequest.Create(Path.Combine(AppVeyorApiUrl, "api/tests"));
jsonData =
string.Format(
"{{" +
"\"testName\": \"{0}\", " +
"\"testFramework\": \"MSTest\", " +
"\"fileName\": \"{0}.py\", " +
"\"outcome\": \"{1}\", " +
"\"durationMilliseconds\": \"{2}\", " +
"\"ErrorMessage\": \"{3}\"" +
"}}",
name,
passed ? "Passed" : "Failed",
durationMS,
error == null ? "" : error
);
var data = Encoding.UTF8.GetBytes(jsonData);
request.Method = "POST";
request.ContentType = "application/json";
request.ContentLength = data.Length;
using (var stream = request.GetRequestStream())
{
stream.Write(data, 0, data.Length);
}
var response = (HttpWebResponse)request.GetResponse();
var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();
}
catch (Exception e)
{
Console.WriteLine("Encountered exception for http post:");
Console.WriteLine(e.Message);
Console.WriteLine(e.StackTrace);
Console.WriteLine("Json content:");
Console.WriteLine(jsonData);
}
}
enum TestResult
{
Fail,
Pass,
PassWithRetry
}
/// <summary>
/// Runs a python test file and returns success/failure
/// </summary>
static async Task<TestResult> RunTest(string file, int index)
{
string pythonPath = "python.exe";
if (AppVeyorMode)
{
if (Environment.GetEnvironmentVariable("Platform").ToLower() == "x64")
{
pythonPath = @"c:\python35-x64\python.exe";
}
else
{
pythonPath = @"c:\python35\python.exe";
}
}
int tries = 0;
Stopwatch Timer;
TestResults Results;
do
{
Timer = new Stopwatch();
Timer.Start();
Results = await ExecuteAsync(pythonPath, file, 30 * 60 * 1000, index);
Timer.Stop();
} while (++tries < RetiresOnFailure + 1 && Results.Pass == false);
if (VerboseOutput)
{
lock (ResultsFolder)
{
foreach (var line in Results.Output)
Console.WriteLine(line);
}
}
UploadAppVeyorTestResult(Path.GetFileNameWithoutExtension(file), Results.Pass, Timer.ElapsedMilliseconds, Results.Error);
// Write the output to a file
var filePrefix = Results.Pass ? "P_" : "F_";
var outputFilePath = Path.Combine(ResultsFolder, filePrefix + Path.GetFileNameWithoutExtension(file) + ".txt");
try {
File.WriteAllLines(outputFilePath, Results.Output);
} catch (Exception e) {
Console.WriteLine("Exception while trying to write {0}:\n{1}!", outputFilePath, e.Message);
}
return Results.Pass ?
(tries == 1 ? TestResult.Pass : TestResult.PassWithRetry) :
TestResult.Fail;
}
/// <summary>
/// Runs all the tests as indicated by input arguments
/// </summary>
static void Main(string[] args)
{
if (args.Length < 2)
{
Console.WriteLine("Usage: otTestRunner.exe [path] [search pattern] (parallel:n) (verbose)");
return;
}
var files = Directory.GetFiles(args[0], args[1]);
if (files.Length == 0)
{
Console.WriteLine("No tests found with that path & pattern!");
return;
}
var NumberOfTestsToRunInParallel = 1;
for (var i = 2; i < args.Length; i++)
{
if (args[i].StartsWith("parallel:"))
NumberOfTestsToRunInParallel = int.Parse(args[i].Substring(9));
else if (args[i].StartsWith("retry:"))
RetiresOnFailure = int.Parse(args[i].Substring(6));
else if (args[i].StartsWith("verbose"))
VerboseOutput = true;
else if (args[i].StartsWith("appveyor"))
{
AppVeyorMode = true;
AppVeyorApiUrl = Environment.GetEnvironmentVariable("APPVEYOR_API_URL");
//Console.WriteLine("AppVeyorApiUrl = {0}", AppVeyorApiUrl);
}
}
var CurNumTestsRunning = 0;
var ReadyToRunEvent = new ManualResetEvent(true);
var TestPassCount = 0;
Stopwatch Timer = new Stopwatch();
Directory.CreateDirectory(ResultsFolder);
Console.WriteLine("Test results saved: .\\{0}", ResultsFolder);
Console.WriteLine("Running {0} tests, {1} at a time:", files.Length, NumberOfTestsToRunInParallel);
/*for (var i = 0; i < files.Length; i++)
Console.WriteLine(Path.GetFileName(files[i]));*/
Console.WriteLine("");
Timer.Start();
for (var i = 0; i < files.Length; i++)
{
// Wait for the event to be set, if not already
ReadyToRunEvent.WaitOne();
if (i != 0)
{
// Wait a bit to stagger the starts
Task.Delay(1000).Wait();
}
lock (ResultsFolder)
{
if (++CurNumTestsRunning == NumberOfTestsToRunInParallel)
ReadyToRunEvent.Reset();
}
var index = i;
var fileName = files[i];
// Start the test, but don't wait for it
Task.Run(
async () =>
{
var result = await RunTest(fileName, index);
lock (ResultsFolder)
{
var PrevColor = Console.ForegroundColor;
if (result != TestResult.Fail)
{
if (result == TestResult.Pass)
{
Console.ForegroundColor = ConsoleColor.Green;
}
else
{
Console.ForegroundColor = ConsoleColor.Yellow;
}
Console.Write("PASS");
TestPassCount++;
}
else
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Write("FAIL");
}
Console.ForegroundColor = PrevColor;
Console.WriteLine(": {0}", Path.GetFileNameWithoutExtension(fileName));
CurNumTestsRunning--;
ReadyToRunEvent.Set();
}
});
}
// Wait for all the tests to complete
while (CurNumTestsRunning != 0)
ReadyToRunEvent.WaitOne();
Timer.Stop();
TimeSpan ts = Timer.Elapsed;
string elapsedTime =
String.Format("{0:00}:{1:00}:{2:00}.{3:00}",
ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds / 10);
Console.WriteLine("{0} tests run in {1}", files.Length, elapsedTime);
Console.WriteLine("{0} passed and {1} failed", TestPassCount, files.Length - TestPassCount);
if (!AppVeyorMode)
Environment.ExitCode = files.Length == TestPassCount ? 0 : 1;
}
}
}