2016-05-26 16:06:52 +03:00
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Runtime.InteropServices ;
using System.Text ;
using System.Text.RegularExpressions ;
using System.Threading ;
using System.Diagnostics ;
using NUnit.Framework ;
namespace Xamarin.Tests
{
class ToolMessage
{
public bool IsError ;
public bool IsWarning { get { return ! IsError ; } }
public string Prefix ;
public int Number ;
public string PrefixedNumber { get { return Prefix + Number . ToString ( ) ; } }
public string Message ;
2017-06-01 02:10:36 +03:00
public string FileName ;
public int LineNumber ;
public override string ToString ( )
{
if ( string . IsNullOrEmpty ( FileName ) ) {
return String . Format ( "{0} {3}{1:0000}: {2}" , IsError ? "error" : "warning" , Number , Message , Prefix ) ;
} else {
return String . Format ( "{3}({4}): {0} {5}{1:0000}: {2}" , IsError ? "error" : "warning" , Number , Message , FileName , LineNumber , Prefix ) ;
}
}
2016-05-26 16:06:52 +03:00
}
class Tool
{
StringBuilder output = new StringBuilder ( ) ;
List < string > output_lines ;
List < ToolMessage > messages = new List < ToolMessage > ( ) ;
public Dictionary < string , string > EnvironmentVariables { get ; set ; }
public TimeSpan Timeout { get ; set ; } = TimeSpan . FromSeconds ( 60 ) ;
public IEnumerable < ToolMessage > Messages { get { return messages ; } }
2017-05-29 17:15:54 +03:00
public List < string > OutputLines {
2016-05-26 16:06:52 +03:00
get {
if ( output_lines = = null ) {
output_lines = new List < string > ( ) ;
output_lines . AddRange ( output . ToString ( ) . Split ( '\n' ) ) ;
}
return output_lines ;
}
}
public int Execute ( string arguments , params string [ ] args )
2016-10-27 15:05:44 +03:00
{
2017-06-01 18:28:56 +03:00
return Execute ( Configuration . MtouchPath , arguments , false , args ) ;
}
public int Execute ( string arguments , bool always_show_output , params string [ ] args )
{
return Execute ( Configuration . MtouchPath , arguments , always_show_output , args ) ;
2016-10-27 15:05:44 +03:00
}
public int Execute ( string toolPath , string arguments , params string [ ] args )
2017-06-01 18:28:56 +03:00
{
return Execute ( toolPath , arguments , false , args ) ;
}
public int Execute ( string toolPath , string arguments , bool always_show_output , params string [ ] args )
2016-05-26 16:06:52 +03:00
{
output . Clear ( ) ;
output_lines = null ;
2016-10-27 15:05:44 +03:00
var rv = ExecutionHelper . Execute ( toolPath , string . Format ( arguments , args ) , EnvironmentVariables , output , output ) ;
2016-05-26 16:06:52 +03:00
2017-06-01 18:28:56 +03:00
if ( ( rv ! = 0 | | always_show_output ) & & output . Length > 0 )
Console . WriteLine ( "\t" + output . ToString ( ) . Replace ( "\n" , "\n\t" ) ) ;
2016-05-26 16:06:52 +03:00
ParseMessages ( ) ;
return rv ;
}
void ParseMessages ( )
{
messages . Clear ( ) ;
foreach ( var l in output . ToString ( ) . Split ( '\n' ) ) {
var line = l ;
var msg = new ToolMessage ( ) ;
2017-06-01 02:10:36 +03:00
var origin = string . Empty ;
if ( line . Contains ( ": error " ) ) {
msg . IsError = true ;
var idx = line . IndexOf ( ": error " , StringComparison . Ordinal ) ;
origin = line . Substring ( 0 , idx ) ;
line = line . Substring ( idx + ": error " . Length ) ;
} else if ( line . Contains ( ": warning " ) ) {
var idx = line . IndexOf ( ": warning " , StringComparison . Ordinal ) ;
origin = line . Substring ( 0 , idx ) ;
line = line . Substring ( idx + ": warning " . Length ) ;
} else if ( line . StartsWith ( "error " , StringComparison . Ordinal ) ) {
2016-05-26 16:06:52 +03:00
msg . IsError = true ;
line = line . Substring ( 6 ) ;
} else if ( line . StartsWith ( "warning " , StringComparison . Ordinal ) ) {
msg . IsError = false ;
line = line . Substring ( 8 ) ;
} else {
// something else
continue ;
}
if ( line . Length < 7 )
continue ; // something else
msg . Prefix = line . Substring ( 0 , 2 ) ;
if ( ! int . TryParse ( line . Substring ( 2 , 4 ) , out msg . Number ) )
continue ; // something else
msg . Message = line . Substring ( 8 ) ;
2017-06-01 02:10:36 +03:00
if ( ! string . IsNullOrEmpty ( origin ) ) {
var idx = origin . IndexOf ( '(' ) ;
if ( idx > 0 ) {
var closing = origin . IndexOf ( ')' ) ;
var number = 0 ;
if ( ! int . TryParse ( origin . Substring ( idx + 1 , closing - idx - 1 ) , out number ) )
continue ;
msg . LineNumber = number ;
msg . FileName = origin . Substring ( 0 , idx ) ;
} else {
msg . FileName = origin ;
}
}
2016-05-26 16:06:52 +03:00
messages . Add ( msg ) ;
}
}
public bool HasErrorPattern ( string prefix , int number , string messagePattern )
{
foreach ( var msg in messages ) {
if ( msg . IsError & & msg . Prefix = = prefix & & msg . Number = = number & & Regex . IsMatch ( msg . Message , messagePattern ) )
return true ;
}
return false ;
}
public bool HasError ( string prefix , int number , string message )
{
foreach ( var msg in messages ) {
if ( msg . IsError & & msg . Prefix = = prefix & & msg . Number = = number & & msg . Message = = message )
return true ;
}
return false ;
}
public void AssertErrorPattern ( int number , string messagePattern )
{
AssertErrorPattern ( "MT" , number , messagePattern ) ;
}
public void AssertErrorPattern ( string prefix , int number , string messagePattern )
{
if ( ! messages . Any ( ( msg ) = > msg . Prefix = = prefix & & msg . Number = = number ) )
Assert . Fail ( string . Format ( "The error '{0}{1:0000}' was not found in the output." , prefix , number ) ) ;
if ( messages . Any ( ( msg ) = > Regex . IsMatch ( msg . Message , messagePattern ) ) )
return ;
var details = messages . Where ( ( msg ) = > msg . Prefix = = prefix & & msg . Number = = number & & ! Regex . IsMatch ( msg . Message , messagePattern ) ) . Select ( ( msg ) = > string . Format ( "\tThe message '{0}' did not match the pattern '{1}'." , msg . Message , messagePattern ) ) ;
Assert . Fail ( string . Format ( "The error '{0}{1:0000}: {2}' was not found in the output:\n{3}" , prefix , number , messagePattern , string . Join ( "\n" , details . ToArray ( ) ) ) ) ;
}
2017-06-01 02:10:36 +03:00
public void AssertError ( int number , string message , string filename = null , int? linenumber = null )
2016-05-26 16:06:52 +03:00
{
2017-06-01 02:10:36 +03:00
AssertError ( "MT" , number , message , filename , linenumber ) ;
2016-05-26 16:06:52 +03:00
}
2017-06-01 02:10:36 +03:00
public void AssertError ( string prefix , int number , string message , string filename = null , int? linenumber = null )
2016-05-26 16:06:52 +03:00
{
if ( ! messages . Any ( ( msg ) = > msg . Prefix = = prefix & & msg . Number = = number ) )
Assert . Fail ( string . Format ( "The error '{0}{1:0000}' was not found in the output." , prefix , number ) ) ;
2017-06-01 02:10:36 +03:00
var matches = messages . Where ( ( msg ) = > msg . Message = = message ) ;
if ( ! matches . Any ( ) ) {
var details = messages .
Where ( ( msg ) = > msg . Prefix = = prefix & & msg . Number = = number & & msg . Message ! = message ) .
Select ( ( msg ) = > string . Format ( "\tMessage #{2} did not match:\n\t\tactual: '{0}'\n\t\texpected: '{1}'" , msg . Message , message , messages . IndexOf ( msg ) + 1 ) ) ;
Assert . Fail ( string . Format ( "The error '{0}{1:0000}: {2}' was not found in the output:\n{3}" , prefix , number , message , string . Join ( "\n" , details . ToArray ( ) ) ) ) ;
}
2016-05-26 16:06:52 +03:00
2017-06-01 02:10:36 +03:00
if ( filename ! = null ) {
var hasDirectory = filename . IndexOf ( Path . DirectorySeparatorChar ) > - 1 ;
if ( ! matches . Any ( ( v ) = > {
if ( hasDirectory ) {
// Check the entire path
return filename = = v . FileName ;
} else {
// Don't compare the directory unless one was specified.
return filename = = Path . GetFileName ( v . FileName ) ;
}
} ) ) {
var details = matches . Select ( ( msg ) = > string . Format ( "\tMessage #{2} did not contain expected filename:\n\t\tactual: '{0}'\n\t\texpected: '{1}'" , hasDirectory ? msg . FileName : Path . GetFileName ( msg . FileName ) , filename , messages . IndexOf ( msg ) + 1 ) ) ;
Assert . Fail ( string . Format ( $"The filename '{filename}' was not found in the output for the error {prefix}{number:X4}: {message}:\n{string.Join (" \ n ", details.ToArray ())}" ) ) ;
}
}
if ( linenumber ! = null ) {
if ( ! matches . Any ( ( v ) = > linenumber . Value = = v . LineNumber ) ) {
var details = matches . Select ( ( msg ) = > string . Format ( "\tMessage #{2} did not contain expected line number:\n\t\tactual: '{0}'\n\t\texpected: '{1}'" , msg . LineNumber , linenumber , messages . IndexOf ( msg ) + 1 ) ) ;
Assert . Fail ( string . Format ( $"The linenumber '{linenumber.Value}' was not found in the output for the error {prefix}{number:X4}: {message}:\n{string.Join (" \ n ", details.ToArray ())}" ) ) ;
}
}
2016-05-26 16:06:52 +03:00
}
2016-12-23 19:09:39 +03:00
public void AssertWarningPattern ( int number , string messagePattern )
{
AssertWarningPattern ( "MT" , number , messagePattern ) ;
}
public void AssertWarningPattern ( string prefix , int number , string messagePattern )
{
if ( ! messages . Any ( ( msg ) = > msg . Prefix = = prefix & & msg . Number = = number ) )
Assert . Fail ( string . Format ( "The warning '{0}{1:0000}' was not found in the output." , prefix , number ) ) ;
if ( messages . Any ( ( msg ) = > Regex . IsMatch ( msg . Message , messagePattern ) ) )
return ;
var details = messages . Where ( ( msg ) = > msg . Prefix = = prefix & & msg . Number = = number & & ! Regex . IsMatch ( msg . Message , messagePattern ) ) . Select ( ( msg ) = > string . Format ( "\tThe message '{0}' did not match the pattern '{1}'." , msg . Message , messagePattern ) ) ;
Assert . Fail ( string . Format ( "The warning '{0}{1:0000}: {2}' was not found in the output:\n{3}" , prefix , number , messagePattern , string . Join ( "\n" , details . ToArray ( ) ) ) ) ;
}
public void AssertWarning ( int number , string message )
{
AssertWarning ( "MT" , number , message ) ;
}
public void AssertWarning ( string prefix , int number , string message )
{
if ( ! messages . Any ( ( msg ) = > msg . Prefix = = prefix & & msg . Number = = number ) )
Assert . Fail ( string . Format ( "The warning '{0}{1:0000}' was not found in the output." , prefix , number ) ) ;
if ( messages . Any ( ( msg ) = > msg . Message = = message ) )
return ;
var details = messages . Where ( ( msg ) = > msg . Prefix = = prefix & & msg . Number = = number & & msg . Message ! = message ) . Select ( ( msg ) = > string . Format ( "\tMessage #{2} did not match:\n\t\tactual: '{0}'\n\t\texpected: '{1}'" , msg . Message , message , messages . IndexOf ( msg ) + 1 ) ) ;
Assert . Fail ( string . Format ( "The warning '{0}{1:0000}: {2}' was not found in the output:\n{3}" , prefix , number , message , string . Join ( "\n" , details . ToArray ( ) ) ) ) ;
}
2017-01-27 09:35:32 +03:00
public void AssertNoWarnings ( )
{
var warnings = messages . Where ( ( v ) = > v . IsWarning ) ;
if ( ! warnings . Any ( ) )
return ;
Assert . Fail ( "No warnings expected, but got:\n{0}\t" , string . Join ( "\n\t" , warnings . Select ( ( v ) = > v . Message ) . ToArray ( ) ) ) ;
}
2016-05-26 16:06:52 +03:00
public bool HasOutput ( string line )
{
return OutputLines . Contains ( line ) ;
}
public bool HasOutputPattern ( string linePattern )
{
foreach ( var line in OutputLines ) {
if ( Regex . IsMatch ( line , linePattern , RegexOptions . CultureInvariant ) )
return true ;
}
return false ;
}
public void AssertOutputPattern ( string linePattern )
{
if ( ! HasOutputPattern ( linePattern ) )
Assert . Fail ( string . Format ( "The output does not contain the line '{0}'" , linePattern ) ) ;
}
2017-04-25 13:49:16 +03:00
public void ForAllOutputLines ( Action < string > action )
{
foreach ( var line in OutputLines )
action ( line ) ;
}
2016-05-26 16:06:52 +03:00
}
class XBuild
{
public static string ToolPath {
get
{
return "/Library/Frameworks/Mono.framework/Commands/xbuild" ;
}
}
2016-11-17 12:22:26 +03:00
public static void Build ( string project , string configuration = "Debug" , string platform = "iPhoneSimulator" , string verbosity = null , TimeSpan ? timeout = null )
2016-05-26 16:06:52 +03:00
{
2016-11-17 12:22:26 +03:00
ExecutionHelper . Execute ( ToolPath , string . Format ( "/p:Configuration={0} /p:Platform={1} {2} \"{3}\"" , configuration , platform , verbosity = = null ? string . Empty : "/verbosity:" + verbosity , project ) , timeout : timeout ) ;
2016-05-26 16:06:52 +03:00
}
}
static class ExecutionHelper {
static int Execute ( ProcessStartInfo psi , StringBuilder stdout , StringBuilder stderr , TimeSpan ? timeout = null )
{
var watch = new Stopwatch ( ) ;
watch . Start ( ) ;
try {
psi . UseShellExecute = false ;
psi . RedirectStandardError = true ;
psi . RedirectStandardOutput = true ;
Console . WriteLine ( "{0} {1}" , psi . FileName , psi . Arguments ) ;
using ( var p = new Process ( ) ) {
p . StartInfo = psi ;
// mtouch/mmp writes UTF8 data outside of the ASCII range, so we need to make sure
// we read it in the same format. This also means we can't use the events to get
// stdout/stderr, because mono's Process class parses those using Encoding.Default.
p . StartInfo . StandardOutputEncoding = Encoding . UTF8 ;
p . StartInfo . StandardErrorEncoding = Encoding . UTF8 ;
p . Start ( ) ;
var outReader = new Thread ( ( ) = >
{
string l ;
while ( ( l = p . StandardOutput . ReadLine ( ) ) ! = null ) {
lock ( stdout )
stdout . AppendLine ( l ) ;
}
} )
{
IsBackground = true ,
} ;
outReader . Start ( ) ;
var errReader = new Thread ( ( ) = >
{
string l ;
while ( ( l = p . StandardError . ReadLine ( ) ) ! = null ) {
lock ( stderr )
stderr . AppendLine ( l ) ;
}
} )
{
IsBackground = true ,
} ;
errReader . Start ( ) ;
if ( timeout = = null )
timeout = TimeSpan . FromMinutes ( 5 ) ;
if ( ! p . WaitForExit ( ( int ) timeout . Value . TotalMilliseconds ) ) {
Console . WriteLine ( "Command didn't finish in {0} minutes:" , timeout . Value . TotalMinutes ) ;
Console . WriteLine ( "{0} {1}" , p . StartInfo . FileName , p . StartInfo . Arguments ) ;
Console . WriteLine ( "Will now kill the process" ) ;
kill ( p . Id , 9 ) ;
if ( ! p . WaitForExit ( 1000 /* killing should be fairly quick */ ) ) {
Console . WriteLine ( "Kill failed to kill in 1 second !?" ) ;
return 1 ;
}
}
outReader . Join ( TimeSpan . FromSeconds ( 1 ) ) ;
errReader . Join ( TimeSpan . FromSeconds ( 1 ) ) ;
return p . ExitCode ;
}
} finally {
Console . WriteLine ( "{0} Executed in {1}: {2} {3}" , DateTime . Now , watch . Elapsed . ToString ( ) , psi . FileName , psi . Arguments ) ;
}
}
public static int Execute ( string fileName , string arguments , out string output , TimeSpan ? timeout = null )
{
var sb = new StringBuilder ( ) ;
var psi = new ProcessStartInfo ( ) ;
psi . FileName = fileName ;
psi . Arguments = arguments ;
var rv = Execute ( psi , sb , sb , timeout ) ;
output = sb . ToString ( ) ;
return rv ;
}
public static int Execute ( string fileName , string arguments , Dictionary < string , string > environmentVariables , StringBuilder stdout , StringBuilder stderr , TimeSpan ? timeout = null )
{
if ( stdout = = null )
stdout = new StringBuilder ( ) ;
if ( stderr = = null )
stderr = new StringBuilder ( ) ;
var psi = new ProcessStartInfo ( ) ;
psi . FileName = fileName ;
psi . Arguments = arguments ;
if ( environmentVariables ! = null ) {
var envs = psi . EnvironmentVariables ;
foreach ( var kvp in environmentVariables ) {
2016-12-23 11:01:11 +03:00
envs [ kvp . Key ] = kvp . Value ;
2016-05-26 16:06:52 +03:00
}
}
return Execute ( psi , stdout , stderr , timeout ) ;
}
[DllImport ("libc")]
private static extern void kill ( int pid , int sig ) ;
public static string Execute ( string fileName , string arguments , bool throwOnError = true , Dictionary < string , string > environmentVariables = null ,
2016-10-07 14:50:48 +03:00
bool hide_output = false , TimeSpan ? timeout = null
2016-05-26 16:06:52 +03:00
)
{
StringBuilder output = new StringBuilder ( ) ;
2016-10-07 14:50:48 +03:00
int exitCode = Execute ( fileName , arguments , environmentVariables , output , output , timeout ) ;
2016-05-26 16:06:52 +03:00
if ( ! hide_output ) {
Console . WriteLine ( "{0} {1}" , fileName , arguments ) ;
Console . WriteLine ( output ) ;
2016-10-07 14:50:48 +03:00
Console . WriteLine ( "Exit code: {0}" , exitCode ) ;
2016-05-26 16:06:52 +03:00
}
if ( throwOnError & & exitCode ! = 0 )
throw new TestExecutionException ( output . ToString ( ) ) ;
return output . ToString ( ) ;
}
}
class TestExecutionException : Exception {
public TestExecutionException ( string output )
: base ( output )
{
}
}
}