commit 416ed4ec4d8479e969b216209e8375d06d34df37 Author: Oren Novotny Date: Mon May 26 13:10:29 2014 -0400 Initial check-in of Xamarin runner apps diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7a6c1d3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,28 @@ +*.ico binary +*.snk binary +*.xls binary + +*.bat text +*.config text +*.cs text diff=csharp +*.csproj text merge=union +*.manifest text +*.msbuild text +*.nuspec text +*.resx text merge=union +*.ruleset text +*.settings text +*.shfb text +*.targets text +*.tdnet text +*.txt text +*.vb text +*.vbproj text merge=union +*.vsixmanifest text +*.vstemplate text +*.xml text +*.xsl text +*.xslt text +*.xunit text + +*.sln text eol=crlf merge=union diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dc911e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +[Bb]in +[Oo]bj +packages +help + +*.suo +*.user +*.[Cc]ache +*[Rr]esharper* +*.zip +*.suo +*.user +*.cache +*.nupkg +*.exe +*.dll +*.ncrunch* + +Test*.html +Test*.xml + +Index.dat +Storage.dat diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..c373b1f --- /dev/null +++ b/license.txt @@ -0,0 +1,13 @@ +Copyright 2014 Outercurve Foundation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/src/CodeAnalysisDictionary.xml b/src/CodeAnalysisDictionary.xml new file mode 100644 index 0000000..010252f --- /dev/null +++ b/src/CodeAnalysisDictionary.xml @@ -0,0 +1,8 @@ + + + + + xunit + + + \ No newline at end of file diff --git a/src/common/Application.ico b/src/common/Application.ico new file mode 100644 index 0000000..6177bdd Binary files /dev/null and b/src/common/Application.ico differ diff --git a/src/common/AssemblyExtensions.cs b/src/common/AssemblyExtensions.cs new file mode 100644 index 0000000..2a6c2a5 --- /dev/null +++ b/src/common/AssemblyExtensions.cs @@ -0,0 +1,23 @@ +using System; +using System.IO; +using System.Reflection; + +internal static class AssemblyExtensions +{ + public static string GetLocalCodeBase(this Assembly assembly) + { + string codeBase = assembly.CodeBase; + if (codeBase == null) + return null; + + if (!codeBase.StartsWith("file:///")) + throw new ArgumentException(String.Format("Code base {0} in wrong format; must start with file:///", codeBase), "assembly"); + + codeBase = codeBase.Substring(8); + if (Path.DirectorySeparatorChar == '/') + return "/" + codeBase; + + return codeBase.Replace('/', Path.DirectorySeparatorChar); + } +} + diff --git a/src/common/DictionaryExtensions.cs b/src/common/DictionaryExtensions.cs new file mode 100644 index 0000000..7e65323 --- /dev/null +++ b/src/common/DictionaryExtensions.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +internal static class DictionaryExtensions +{ + public static void Add(this IDictionary> dictionary, TKey key, TValue value) + { + dictionary.GetOrAdd(key).Add(value); + } + + public static bool Contains(this IDictionary> dictionary, TKey key, TValue value, IEqualityComparer valueComparer) + { + List values; + + if (!dictionary.TryGetValue(key, out values)) + return false; + + return values.Contains(value, valueComparer); + } + + public static TValue GetOrAdd(this IDictionary dictionary, TKey key) + where TValue : new() + { + return dictionary.GetOrAdd(key, () => new TValue()); + } + + public static TValue GetOrAdd(this IDictionary dictionary, TKey key, Func newValue) + { + TValue result; + + if (!dictionary.TryGetValue(key, out result)) + { + result = newValue(); + dictionary[key] = result; + } + + return result; + } +} diff --git a/src/common/DisposableExtensions.cs b/src/common/DisposableExtensions.cs new file mode 100644 index 0000000..5e8c96e --- /dev/null +++ b/src/common/DisposableExtensions.cs @@ -0,0 +1,10 @@ +using System; + +internal static class DisposableExtensions +{ + public static void SafeDispose(this IDisposable disposable) + { + if (disposable != null) + disposable.Dispose(); + } +} \ No newline at end of file diff --git a/src/common/ExceptionExtensions.cs b/src/common/ExceptionExtensions.cs new file mode 100644 index 0000000..2e53341 --- /dev/null +++ b/src/common/ExceptionExtensions.cs @@ -0,0 +1,47 @@ +using System; +using System.Reflection; + +internal static class ExceptionExtensions +{ + const string RETHROW_MARKER = "$$RethrowMarker$$"; + + /// + /// Rethrows an exception object without losing the existing stack trace information + /// + /// The exception to re-throw. + /// + /// For more information on this technique, see + /// http://www.dotnetjunkies.com/WebLog/chris.taylor/archive/2004/03/03/8353.aspx. + /// The remote_stack_trace string is here to support Mono. + /// + public static void RethrowWithNoStackTraceLoss(this Exception ex) + { +#if XUNIT_CORE_DLL + System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(ex).Throw(); +#else + FieldInfo remoteStackTraceString = + typeof(Exception).GetField("_remoteStackTraceString", BindingFlags.Instance | BindingFlags.NonPublic) ?? + typeof(Exception).GetField("remote_stack_trace", BindingFlags.Instance | BindingFlags.NonPublic); + + remoteStackTraceString.SetValue(ex, ex.StackTrace + RETHROW_MARKER); + throw ex; +#endif + } + + /// + /// Unwraps an exception to remove any wrappers, like . + /// + /// The exception to unwrap. + /// The unwrapped exception. + public static Exception Unwrap(this Exception ex) + { + while (true) + { + var tiex = ex as TargetInvocationException; + if (tiex == null) + return ex; + + ex = tiex.InnerException; + } + } +} \ No newline at end of file diff --git a/src/common/GlobalAssemblyInfo.cs b/src/common/GlobalAssemblyInfo.cs new file mode 100644 index 0000000..875dfb8 --- /dev/null +++ b/src/common/GlobalAssemblyInfo.cs @@ -0,0 +1,13 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +[assembly: AssemblyCompany("Outercurve Foundation")] +[assembly: AssemblyProduct("xUnit.net Testing Framework")] +[assembly: AssemblyCopyright("Copyright (C) Outercurve Foundation")] +[assembly: AssemblyVersion("2.0.0.0")] + +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "Xunit.Sdk")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "xunit")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "extensions")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "utility")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "runner")] \ No newline at end of file diff --git a/src/common/Guard.cs b/src/common/Guard.cs new file mode 100644 index 0000000..c591b02 --- /dev/null +++ b/src/common/Guard.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections; +using System.Diagnostics.CodeAnalysis; +using System.IO; + +/// +/// Guard class, used for guard clauses and argument validation +/// +internal static class Guard +{ + /// + public static void ArgumentNotNull(string argName, object argValue) + { + if (argValue == null) + throw new ArgumentNullException(argName); + } + + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This method may not be called by all users of Guard.")] + public static void ArgumentNotNullOrEmpty(string argName, IEnumerable argValue) + { + ArgumentNotNull(argName, argValue); + + if (!argValue.GetEnumerator().MoveNext()) + throw new ArgumentException("Argument was empty", argName); + } + + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This method may not be called by all users of Guard.")] + public static void ArgumentValid(string argName, string message, bool test) + { + if (!test) + throw new ArgumentException(message, argName); + } + +#if !XUNIT_CORE_DLL + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This method may not be called by all users of Guard.")] + public static void FileExists(string argName, string fileName) + { + Guard.ArgumentNotNullOrEmpty(argName, fileName); + Guard.ArgumentValid("assemblyFileName", + String.Format("File not found: {0}", fileName), + File.Exists(fileName)); + } +#endif +} \ No newline at end of file diff --git a/src/common/SerializationInfoExtensions.cs b/src/common/SerializationInfoExtensions.cs new file mode 100644 index 0000000..5617d8a --- /dev/null +++ b/src/common/SerializationInfoExtensions.cs @@ -0,0 +1,9 @@ +using System.Runtime.Serialization; + +internal static class SerializationInfoExtensions +{ + public static T GetValue(this SerializationInfo info, string name) + { + return (T)info.GetValue(name, typeof(T)); + } +} \ No newline at end of file diff --git a/src/common/SourceInformation.cs b/src/common/SourceInformation.cs new file mode 100644 index 0000000..f84df32 --- /dev/null +++ b/src/common/SourceInformation.cs @@ -0,0 +1,42 @@ +using System; +using System.Runtime.Serialization; +using Xunit.Abstractions; + +#if XUNIT_CORE_DLL +namespace Xunit.Sdk +#else +namespace Xunit +#endif +{ + /// + /// Default implementation of . + /// + [Serializable] + public class SourceInformation : LongLivedMarshalByRefObject, ISourceInformation, ISerializable + { + /// + /// Initializes a new instance of the class. + /// + public SourceInformation() { } + + /// + protected SourceInformation(SerializationInfo info, StreamingContext context) + { + FileName = info.GetString("FileName"); + LineNumber = (int?)info.GetValue("LineNumber", typeof(int?)); + } + + /// + public string FileName { get; set; } + + /// + public int? LineNumber { get; set; } + + /// + public void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue("FileName", FileName); + info.AddValue("LineNumber", LineNumber, typeof(int?)); + } + } +} \ No newline at end of file diff --git a/src/common/TestDiscoveryVisitor.cs b/src/common/TestDiscoveryVisitor.cs new file mode 100644 index 0000000..6440b2e --- /dev/null +++ b/src/common/TestDiscoveryVisitor.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Xunit.Abstractions; + +namespace Xunit +{ + internal class TestDiscoveryVisitor : TestMessageVisitor + { + public TestDiscoveryVisitor() + { + TestCases = new List(); + } + + public List TestCases { get; private set; } + + public override void Dispose() + { + TestCases.ForEach(testCase => testCase.Dispose()); + TestCases = null; + } + + protected override bool Visit(ITestCaseDiscoveryMessage discovery) + { + TestCases.Add(discovery.TestCase); + + return true; + } + } +} \ No newline at end of file diff --git a/src/common/TestOptionsNames.cs b/src/common/TestOptionsNames.cs new file mode 100644 index 0000000..9e3e6f1 --- /dev/null +++ b/src/common/TestOptionsNames.cs @@ -0,0 +1,13 @@ +internal static class TestOptionsNames +{ + internal static class Discovery + { + } + + internal static class Execution + { + public static readonly string SynchronousMessageReporting = "xunit.SynchronousMessageReporting"; + public static readonly string DisableParallelization = "xunit.DisableParallelization"; + public static readonly string MaxParallelThreads = "xunit.MaxParallelThreads"; + } +} \ No newline at end of file diff --git a/src/common/XmlTestExecutionVisitor.cs b/src/common/XmlTestExecutionVisitor.cs new file mode 100644 index 0000000..e6cd185 --- /dev/null +++ b/src/common/XmlTestExecutionVisitor.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Concurrent; +using System.Xml.Linq; +using Xunit.Abstractions; + +namespace Xunit +{ + public class XmlTestExecutionVisitor : TestMessageVisitor + { + readonly XElement assemblyElement; + readonly ConcurrentDictionary testCollectionElements = new ConcurrentDictionary(); + + public XmlTestExecutionVisitor(XElement assemblyElement, Func cancelThunk) + { + CancelThunk = cancelThunk ?? (() => false); + + this.assemblyElement = assemblyElement; + } + + public readonly Func CancelThunk; + public int Failed; + public int Skipped; + public decimal Time; + public int Total; + + XElement CreateTestResultElement(ITestResultMessage testResult, string resultText) + { + var collectionElement = GetTestCollectionElement(testResult.TestCase.TestCollection); + var testResultElement = + new XElement("test", + new XAttribute("name", XmlEscape(testResult.TestDisplayName)), + new XAttribute("type", testResult.TestCase.Class.Name), + new XAttribute("method", testResult.TestCase.Method.Name), + new XAttribute("time", testResult.ExecutionTime.ToString("0.000")), + new XAttribute("result", resultText) + ); + + if (testResult.TestCase.SourceInformation != null) + { + if (testResult.TestCase.SourceInformation.FileName != null) + testResultElement.Add(new XAttribute("source-file", testResult.TestCase.SourceInformation.FileName)); + if (testResult.TestCase.SourceInformation.LineNumber != null) + testResultElement.Add(new XAttribute("source-line", testResult.TestCase.SourceInformation.LineNumber.GetValueOrDefault())); + } + + if (testResult.TestCase.Traits != null && testResult.TestCase.Traits.Count > 0) + { + var traitsElement = new XElement("traits"); + + foreach (var key in testResult.TestCase.Traits.Keys) + foreach (var value in testResult.TestCase.Traits[key]) + traitsElement.Add( + new XElement("trait", + new XAttribute("name", XmlEscape(key)), + new XAttribute("value", XmlEscape(value)) + ) + ); + + testResultElement.Add(traitsElement); + } + + collectionElement.Add(testResultElement); + + return testResultElement; + } + + XElement GetTestCollectionElement(ITestCollection testCollection) + { + return testCollectionElements.GetOrAdd(testCollection, tc => new XElement("collection")); + } + + public override bool OnMessage(IMessageSinkMessage message) + { + var result = base.OnMessage(message); + if (result) + result = !CancelThunk(); + + return result; + } + + protected override bool Visit(ITestAssemblyFinished assemblyFinished) + { + Total += assemblyFinished.TestsRun; + Failed += assemblyFinished.TestsFailed; + Skipped += assemblyFinished.TestsSkipped; + Time += assemblyFinished.ExecutionTime; + + if (assemblyElement != null) + { + assemblyElement.Add( + new XAttribute("total", Total), + new XAttribute("passed", Total - Failed - Skipped), + new XAttribute("failed", Failed), + new XAttribute("skipped", Skipped), + new XAttribute("time", Time.ToString("0.000")) + ); + + foreach (var element in testCollectionElements.Values) + assemblyElement.Add(element); + } + + return base.Visit(assemblyFinished); + } + + protected override bool Visit(ITestAssemblyStarting assemblyStarting) + { + if (assemblyElement != null) + { + assemblyElement.Add( + new XAttribute("name", assemblyStarting.AssemblyFileName), + new XAttribute("environment", assemblyStarting.TestEnvironment), + new XAttribute("test-framework", assemblyStarting.TestFrameworkDisplayName), + new XAttribute("run-date", assemblyStarting.StartTime.ToString("yyyy-MM-dd")), + new XAttribute("run-time", assemblyStarting.StartTime.ToString("HH:mm:ss")) + ); + + if (assemblyStarting.ConfigFileName != null) + assemblyElement.Add(new XAttribute("config-file", assemblyStarting.ConfigFileName)); + } + + return base.Visit(assemblyStarting); + } + + protected override bool Visit(ITestCollectionFinished testCollectionFinished) + { + if (assemblyElement != null) + { + var collectionElement = GetTestCollectionElement(testCollectionFinished.TestCollection); + collectionElement.Add( + new XAttribute("total", testCollectionFinished.TestsRun), + new XAttribute("passed", testCollectionFinished.TestsRun - testCollectionFinished.TestsFailed - testCollectionFinished.TestsSkipped), + new XAttribute("failed", testCollectionFinished.TestsFailed), + new XAttribute("skipped", testCollectionFinished.TestsSkipped), + new XAttribute("name", XmlEscape(testCollectionFinished.TestCollection.DisplayName)), + new XAttribute("time", testCollectionFinished.ExecutionTime.ToString("0.000")) + ); + } + + return base.Visit(testCollectionFinished); + } + + protected override bool Visit(ITestFailed testFailed) + { + if (assemblyElement != null) + { + var testElement = CreateTestResultElement(testFailed, "Fail"); + testElement.Add( + new XElement("failure", + new XAttribute("exception-type", testFailed.ExceptionTypes[0]), + new XElement("message", new XCData(XmlEscape(ExceptionUtility.CombineMessages(testFailed)))), + new XElement("stack-trace", new XCData(ExceptionUtility.CombineStackTraces(testFailed) ?? String.Empty)) + ) + ); + } + + return base.Visit(testFailed); + } + + protected override bool Visit(ITestPassed testPassed) + { + if (assemblyElement != null) + CreateTestResultElement(testPassed, "Pass"); + + return base.Visit(testPassed); + } + + protected override bool Visit(ITestSkipped testSkipped) + { + if (assemblyElement != null) + { + var testElement = CreateTestResultElement(testSkipped, "Skip"); + testElement.Add(new XElement("reason", new XCData(XmlEscape(testSkipped.Reason)))); + } + + return base.Visit(testSkipped); + } + + protected static string Escape(string value) + { + if (value == null) + return String.Empty; + + return value.Replace("\r", "\\r").Replace("\n", "\\n").Replace("\t", "\\t").Replace("\0", "\\0"); + } + + protected static string XmlEscape(string value) + { + if (value == null) + return String.Empty; + + return value.Replace("\0", "\\0"); + } + } +} \ No newline at end of file diff --git a/src/xunit.ruleset b/src/xunit.ruleset new file mode 100644 index 0000000..4d93325 --- /dev/null +++ b/src/xunit.ruleset @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/xunit.runner.android/Activities/CreditsActivity.cs b/src/xunit.runner.android/Activities/CreditsActivity.cs new file mode 100644 index 0000000..d0b266a --- /dev/null +++ b/src/xunit.runner.android/Activities/CreditsActivity.cs @@ -0,0 +1,46 @@ +// +// Copyright 2011-2012 Xamarin Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +using Android.App; +using Android.OS; + +using MonoDroid.Dialog; + +namespace Xunit.Runners.UI +{ + + [Activity (Label = "Credits")] + internal class CreditsActivity : DialogActivity { + + const string notice = "
xUnit Android Runner
Copyright © 2014
Outercurve Foundation
All rights reserved.

Author: Oren Novotny
"; + + protected override void OnCreate (Bundle bundle) + { + Root = new RootElement (String.Empty) { + new FormattedSection (notice) { + new HtmlElement ("About Xamarin", "http://xamarin.com"), + new HtmlElement ("About Mono for Android", "http://android.xamarin.com"), + new HtmlElement ("About MonoDroid.Dialog", "https://github.com/spouliot/MonoDroid.Dialog"), + new HtmlElement("About xUnit", "https://github.com/xunit/xunit"), + } + }; + + base.OnCreate (bundle); + } + } +} \ No newline at end of file diff --git a/src/xunit.runner.android/Activities/OptionsActivity.cs b/src/xunit.runner.android/Activities/OptionsActivity.cs new file mode 100644 index 0000000..833c49b --- /dev/null +++ b/src/xunit.runner.android/Activities/OptionsActivity.cs @@ -0,0 +1,103 @@ +// +// Copyright 2011-2012 Xamarin Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +using Android.App; +using Android.Content; +using Android.Content.PM; +using Android.OS; +using Android.Views; + +using MonoDroid.Dialog; + +namespace Xunit.Runners.UI +{ + + [Activity (Label = "Options", WindowSoftInputMode = SoftInput.AdjustPan, + ConfigurationChanges = ConfigChanges.KeyboardHidden | ConfigChanges.Orientation)] + internal class OptionsActivity : DialogActivity { + BooleanElement remote; + EntryElement host_name; + EntryElement host_port; + + protected override void OnCreate (Bundle bundle) + { + RunnerOptions options = AndroidRunner.Runner.Options; + + var nameDisplayGroup = new RadioGroup("nameDisplay", options.NameDisplay == NameDisplay.Short ? 1 : 0); ; + var nameDisplayFull = new RadioElement("Full", "nameDisplay"); + var nameDisplayShort = new RadioElement("Short", "nameDisplay"); + + remote = new BooleanElement ("Remote Server", options.EnableNetwork); + host_name = new EntryElement ("HostName", options.HostName); + host_port = new EntryElement ("Port", options.HostPort.ToString ()) { Numeric = true }; + + var par = new BooleanElement("Parallelize Assemblies", options.ParallelizeAssemblies); + + + Root = new RootElement("Options") + { + new Section() + { + remote, + host_name, + host_port + }, + + new Section("Execution") { par }, + + new Section("Display") + { + new[] + { + new RootElement("Name Display", nameDisplayGroup) + { + new Section() + { + nameDisplayFull, + nameDisplayShort + } + } + } + } + }; + + base.OnCreate (bundle); + } + + int GetPort () + { + int port; + ushort p; + if (UInt16.TryParse (host_port.Value, out p)) + port = p; + else + port = -1; + return port; + } + + protected override void OnPause () + { + var options = AndroidRunner.Runner.Options; + options.EnableNetwork = remote.Value; + options.HostName = host_name.Value; + options.HostPort = GetPort (); + options.Save (this); + base.OnPause (); + } + } +} \ No newline at end of file diff --git a/src/xunit.runner.android/Activities/RunnerActivity.cs b/src/xunit.runner.android/Activities/RunnerActivity.cs new file mode 100644 index 0000000..0347b94 --- /dev/null +++ b/src/xunit.runner.android/Activities/RunnerActivity.cs @@ -0,0 +1,76 @@ +// +// Copyright 2011-2012 Xamarin Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Android.App; +using Android.OS; +using Android.Widget; +using MonoDroid.Dialog; + +namespace Xunit.Runners.UI +{ + public class RunnerActivity : Activity + { + private Assembly assembly; + + public RunnerActivity() + { + //Initialized = (AndroidRunner.AssemblyLevel.Count > 0); + } + + public bool Initialized { get; private set; } + + public AndroidRunner Runner + { + get { return AndroidRunner.Runner; } + } + + protected override void OnCreate(Bundle bundle) + { + base.OnCreate(bundle); + + var view = Runner.GetView(this); + + Initialized = true; + + SetContentView(view); + } + + public void Add(Assembly assembly) + { + if (assembly == null) + throw new ArgumentNullException("assembly"); + + // this can be called many times but we only want to load them + // once since we need to share them across most activities + if (!Initialized) + { + AndroidRunner.AddAssembly(assembly); + } + } + + public void AddExecutionAssembly(Assembly assembly) + { + if (assembly == null) throw new ArgumentNullException("assembly"); + this.assembly = assembly; + } + } +} \ No newline at end of file diff --git a/src/xunit.runner.android/Activities/TestResultActivity.cs b/src/xunit.runner.android/Activities/TestResultActivity.cs new file mode 100644 index 0000000..5175fd0 --- /dev/null +++ b/src/xunit.runner.android/Activities/TestResultActivity.cs @@ -0,0 +1,70 @@ +// +// Copyright 2011-2012 Xamarin Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Android.App; +using Android.OS; +using Android.Widget; +using MonoDroid.Dialog; +using Xunit.Abstractions; +using Environment = System.Environment; + +namespace Xunit.Runners.UI +{ + [Activity(Label = "Results")] + public class TestResultActivity : Activity + { + protected override void OnCreate(Bundle bundle) + { + base.OnCreate(bundle); + + var testCaseUniqueId = Intent.GetStringExtra("TestCase"); + + var result = AndroidRunner.Runner.Results[testCaseUniqueId]; + + var message = string.Empty; + if (result.TestCase.Result == TestState.Failed) + { + message = String.Format("{0}
{1}", + result.ErrorMessage, result.ErrorStackTrace.Replace(Environment.NewLine, "
")); + } + else if (result.TestCase.Result == TestState.Skipped) + { + message = String.Format("{0}", + ((ITestSkipped) result.TestResultMessage).Reason); + } + + + var menu = new RootElement(String.Empty) + { + new Section(result.TestCase.DisplayName) + { + new FormattedElement(message) + } + }; + + var da = new DialogAdapter(this, menu); + var lv = new ListView(this) + { + Adapter = da + }; + SetContentView(lv); + } + } +} \ No newline at end of file diff --git a/src/xunit.runner.android/Activities/TestSuiteActivity.cs b/src/xunit.runner.android/Activities/TestSuiteActivity.cs new file mode 100644 index 0000000..ec9be66 --- /dev/null +++ b/src/xunit.runner.android/Activities/TestSuiteActivity.cs @@ -0,0 +1,74 @@ +// +// Copyright 2011-2012 Xamarin Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Android.App; +using Android.OS; +using Android.Widget; +using MonoDroid.Dialog; + +namespace Xunit.Runners.UI +{ + [Activity(Label = "Tests")] + public class TestSuiteActivity : Activity + { + private Section main; + private string sourceName; + private TestSuiteElement suiteElement; + + protected override void OnCreate(Bundle bundle) + { + base.OnCreate(bundle); + + sourceName = Intent.GetStringExtra("TestSuite"); + suiteElement = AndroidRunner.Runner.Suites[sourceName]; + + var menu = new RootElement(String.Empty); + + main = new Section(sourceName); + foreach (var test in suiteElement.TestCases) + { + main.Add(test); + } + menu.Add(main); + + var options = new Section() + { + new ActionElement("Run all", Run), + }; + menu.Add(options); + + var da = new DialogAdapter(this, menu); + var lv = new ListView(this) + { + Adapter = da + }; + SetContentView(lv); + } + + private async void Run() + { + var runner = AndroidRunner.Runner; + + await runner.Run(suiteElement.TestCases.Select(tc => tc.TestCase)); + + suiteElement.Refresh(); + } + } +} \ No newline at end of file diff --git a/src/xunit.runner.android/AndroidRunner.cs b/src/xunit.runner.android/AndroidRunner.cs new file mode 100644 index 0000000..193247f --- /dev/null +++ b/src/xunit.runner.android/AndroidRunner.cs @@ -0,0 +1,535 @@ +// +// Copyright 2011-2012 Xamarin Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Sockets; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Android.App; +using Android.OS; +using Android.Views; +using Android.Widget; +using MonoDroid.Dialog; +using Xunit.Runners.UI; +using Xunit.Runners.Utilities; +using Xunit.Runners.Visitors; +using Debug = System.Diagnostics.Debug; + + +namespace Xunit.Runners +{ + public class AndroidRunner : ITestListener + { + private static readonly AndroidRunner runner = new AndroidRunner(); + private static readonly List assemblies = new List(); + + private readonly AsyncLock executionLock = new AsyncLock(); + private readonly ManualResetEvent mre = new ManualResetEvent(false); + private readonly Dictionary results = new Dictionary(); + private readonly Dictionary suiteElements = new Dictionary(); + private bool cancelled; + private int failed; + private RunnerOptions options; + private int passed; + private int skipped; + private Dictionary> testCasesByAssembly = new Dictionary>(); + + private AndroidRunner() + { + } + + public bool AutoStart { get; set; } + + public bool TerminateAfterExecution { get; set; } + + public RunnerOptions Options + { + get + { + if (options == null) + options = new RunnerOptions(); + return options; + } + set { options = value; } + } + + + public static AndroidRunner Runner + { + get { return runner; } + } + + public IDictionary Results + { + get { return results; } + } + + internal IDictionary Suites + { + get { return suiteElements; } + } + + public TextWriter Writer { get; set; } + + private bool OpenWriter(string message) + { + var now = DateTime.Now; + // let the application provide it's own TextWriter to ease automation with AutoStart property + if (Writer == null) + { + if (Options.ShowUseNetworkLogger) + { + Console.WriteLine("[{0}] Sending '{1}' results to {2}:{3}", now, message, Options.HostName, Options.HostPort); + try + { + Writer = new TcpTextWriter(Options.HostName, Options.HostPort); + } + catch (SocketException) + { + var msg = String.Format("Cannot connect to {0}:{1}. Start network service or disable network option", options.HostName, options.HostPort); + Toast.MakeText(Application.Context, msg, ToastLength.Long) + .Show(); + return false; + } + } + else + { + Writer = Console.Out; + } + } + + Writer.WriteLine("[Runner executing:\t{0}]", message); + // FIXME + Writer.WriteLine("[M4A Version:\t{0}]", "???"); + + Writer.WriteLine("[Board:\t\t{0}]", Build.Board); + Writer.WriteLine("[Bootloader:\t{0}]", Build.Bootloader); + Writer.WriteLine("[Brand:\t\t{0}]", Build.Brand); + Writer.WriteLine("[CpuAbi:\t{0} {1}]", Build.CpuAbi, Build.CpuAbi2); + Writer.WriteLine("[Device:\t{0}]", Build.Device); + Writer.WriteLine("[Display:\t{0}]", Build.Display); + Writer.WriteLine("[Fingerprint:\t{0}]", Build.Fingerprint); + Writer.WriteLine("[Hardware:\t{0}]", Build.Hardware); + Writer.WriteLine("[Host:\t\t{0}]", Build.Host); + Writer.WriteLine("[Id:\t\t{0}]", Build.Id); + Writer.WriteLine("[Manufacturer:\t{0}]", Build.Manufacturer); + Writer.WriteLine("[Model:\t\t{0}]", Build.Model); + Writer.WriteLine("[Product:\t{0}]", Build.Product); + Writer.WriteLine("[Radio:\t\t{0}]", Build.Radio); + Writer.WriteLine("[Tags:\t\t{0}]", Build.Tags); + Writer.WriteLine("[Time:\t\t{0}]", Build.Time); + Writer.WriteLine("[Type:\t\t{0}]", Build.Type); + Writer.WriteLine("[User:\t\t{0}]", Build.User); + Writer.WriteLine("[VERSION.Codename:\t{0}]", Build.VERSION.Codename); + Writer.WriteLine("[VERSION.Incremental:\t{0}]", Build.VERSION.Incremental); + Writer.WriteLine("[VERSION.Release:\t{0}]", Build.VERSION.Release); + Writer.WriteLine("[VERSION.Sdk:\t\t{0}]", Build.VERSION.Sdk); + Writer.WriteLine("[VERSION.SdkInt:\t{0}]", Build.VERSION.SdkInt); + Writer.WriteLine("[Device Date/Time:\t{0}]", now); // to match earlier C.WL output + + // FIXME: add data about how the app was compiled (e.g. ARMvX, LLVM, Linker options) + + return true; + } + + private void CloseWriter() + { + Writer.Close(); + Writer = null; + } + + void ITestListener.RecordResult(MonoTestResult result) + { + Application.SynchronizationContext.Post(_ => + { + Results[result.TestCase.UniqueName] = result; + + result.RaiseTestUpdated(); + }, null); + + if (result.TestCase.Result == TestState.Passed) + { + Writer.Write("\t[PASS] "); + passed++; + } + else if (result.TestCase.Result == TestState.Skipped) + { + Writer.Write("\t[SKIPPED] "); + skipped++; + } + else if (result.TestCase.Result == TestState.Failed) + { + Writer.Write("\t[FAIL] "); + failed++; + } + else + { + Writer.Write("\t[INFO] "); + } + Writer.Write(result.TestCase.DisplayName); + + var message = result.ErrorMessage; + if (!String.IsNullOrEmpty(message)) + { + Writer.Write(" : {0}", message.Replace("\r\n", "\\r\\n")); + } + Writer.WriteLine(); + + var stacktrace = result.ErrorStackTrace; + if (!String.IsNullOrEmpty(result.ErrorStackTrace)) + { + var lines = stacktrace.Split(new char[] {'\r', '\n'}, StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + Writer.WriteLine("\t\t{0}", line); + } + } + + private IEnumerable> DiscoverTestsInAssemblies() + { + var stopwatch = Stopwatch.StartNew(); + var result = new List>(); + + try + { + using (AssemblyHelper.SubscribeResolve()) + { + foreach (var assm in assemblies) + { + // Xunit needs the file name + var fileName = Path.GetFileName(assm.Location); + + try + { + using (var framework = new XunitFrontController(fileName, configFileName: null, shadowCopy: true)) + using (var sink = new TestDiscoveryVisitor()) + { + framework.Find(includeSourceInformation: true, messageSink: sink, options: new TestFrameworkOptions()); + sink.Finished.WaitOne(); + + result.Add( + new Grouping( + fileName, + sink.TestCases + .GroupBy(tc => String.Format("{0}.{1}", tc.Class.Name, tc.Method.Name)) + .SelectMany(group => + group.Select(testCase => + new MonoTestCase(fileName, testCase, forceUniqueNames: group.Count() > 1))) + .ToList() + ) + ); + } + } + catch (Exception e) + { + Debug.WriteLine(e); + } + } + } + } + catch (Exception e) + { + Debug.WriteLine(e); + } + + stopwatch.Stop(); + + return result; + } + + internal View GetView(Activity activity) + { + if (Options == null) + Options = new RunnerOptions(activity); + + RunnerOptions.Initialize(activity); + + Results.Clear(); + suiteElements.Clear(); + + var menu = new RootElement("Test Runner"); + + var main = new Section("Loading test assemblies..."); + + var optSect = new Section() + { + new ActivityElement("Options", typeof(OptionsActivity)), + new ActivityElement("Credits", typeof(CreditsActivity)) + }; + + menu.Add(main); + menu.Add(optSect); + + var a = new DialogAdapter(activity, menu); + var lv = new ListView(activity) + { + Adapter = a + }; + + + ThreadPool.QueueUserWorkItem(_ => + { + var allTests = DiscoverTestsInAssemblies(); + testCasesByAssembly = allTests.ToDictionary(cases => cases.Key, cases => cases as IEnumerable); + + + activity.RunOnUiThread(() => + { + foreach (var kvp in testCasesByAssembly) + { + main.Add(SetupSource(kvp.Key, kvp.Value)); + } + + + mre.Set(); + main.Caption = null; + + optSect.Insert(0, new ActionElement("Run Everything", async () => await Run())); + + a.NotifyDataSetChanged(); + }); + + assemblies.Clear(); + }); + + + // AutoStart running the tests (with either the supplied 'writer' or the options) + if (AutoStart) + { + ThreadPool.QueueUserWorkItem(delegate + { + mre.WaitOne(); + activity.RunOnUiThread(async () => + { + await Run(); + + // optionally end the process, + if (TerminateAfterExecution) + activity.Finish(); + }); + }); + } + + return lv; + } + + internal static void AddAssembly(Assembly testAssm) + { + assemblies.Add(testAssm); + } + + internal Task Run() + { + return Run(testCasesByAssembly.Values.SelectMany(v => v), "Run Everything"); + } + + internal Task Run(MonoTestCase test) + { + return Run(new[] {test}); + } + + internal async Task Run(IEnumerable tests, string message = null) + { + var stopWatch = Stopwatch.StartNew(); + + var groups = tests.GroupBy(t => t.AssemblyFileName); + + using (await executionLock.LockAsync()) + { + if (message == null) + message = tests.Count() > 1 ? "Run Multiple Tests" : tests.First() + .DisplayName; + if (!OpenWriter(message)) + return; + try + { + await RunTests(groups, stopWatch); + } + finally + { + CloseWriter(); + } + } + } + + private TestSuiteElement SetupSource(string sourceName, IEnumerable testSource) + { + var root = new RootElement("Tests"); + + var elements = new List(); + + var section = new Section(sourceName); + foreach (var test in testSource) + { + var ele = new TestCaseElement(test, this); + elements.Add(ele); + section.Add(ele); + } + + var tse = new TestSuiteElement(sourceName, elements, this); + suiteElements[sourceName] = tse; + + + root.Add(section); + + if (section.Count > 1) + { + StringElement allbtn = null; + allbtn = new StringElement("Run all", + async delegate + { + await Run(testSource); + }); + var options = new Section() + { + allbtn + }; + + root.Add(options); + } + return tse; + } + + + private Task RunTests(IEnumerable> testCaseAccessor, Stopwatch stopwatch) + { + var tcs = new TaskCompletionSource(null); + + ThreadPool.QueueUserWorkItem(state => + { + var toDispose = new List(); + + try + { + cancelled = false; + + using (AssemblyHelper.SubscribeResolve()) + if (RunnerOptions.Current.ParallelizeAssemblies) + testCaseAccessor + .Select(testCaseGroup => RunTestsInAssemblyAsync(toDispose, testCaseGroup.Key, testCaseGroup, stopwatch)) + .ToList() + .ForEach(@event => @event.WaitOne()); + else + testCaseAccessor + .ToList() + .ForEach(testCaseGroup => RunTestsInAssembly(toDispose, testCaseGroup.Key, testCaseGroup, stopwatch)); + } + catch (Exception e) + { + tcs.SetException(e); + } + finally + { + toDispose.ForEach(disposable => disposable.Dispose()); + OnTestRunCompleted(); + tcs.SetResult(null); + } + }); + + return tcs.Task; + } + + private ManualResetEvent RunTestsInAssemblyAsync(List toDispose, + string assemblyFileName, + IEnumerable testCases, + Stopwatch stopwatch) + { + var @event = new ManualResetEvent(initialState: false); + + ThreadPool.QueueUserWorkItem(_ => + { + try + { + RunTestsInAssembly(toDispose, assemblyFileName, testCases, stopwatch); + } + finally + { + @event.Set(); + } + }); + + return @event; + } + + private void RunTestsInAssembly(List toDispose, + string assemblyFileName, + IEnumerable testCases, + Stopwatch stopwatch) + { + if (cancelled) + return; + + var controller = new XunitFrontController(assemblyFileName, configFileName: null, shadowCopy: true); + + lock (toDispose) + toDispose.Add(controller); + + var xunitTestCases = testCases.ToDictionary(tc => tc.TestCase); + + using (var executionVisitor = new MonoTestExecutionVisitor(xunitTestCases, this, () => cancelled)) + { + var executionOptions = new XunitExecutionOptions + { + //DisableParallelization = !settings.ParallelizeTestCollections, + //MaxParallelThreads = settings.MaxParallelThreads + }; + + controller.RunTests(xunitTestCases.Keys.ToList(), executionVisitor, executionOptions); + executionVisitor.Finished.WaitOne(); + } + } + + private void OnTestRunCompleted() + { + Application.SynchronizationContext.Post(_ => + { + foreach (var ts in suiteElements.Values) + { + // Recalc the status + ts.Refresh(); + } + }, null); + } + + private class Grouping : IGrouping + { + private readonly IEnumerable elements; + + public Grouping(TKey key, IEnumerable elements) + { + Key = key; + this.elements = elements; + } + + public TKey Key { get; private set; } + + public IEnumerator GetEnumerator() + { + return elements.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return elements.GetEnumerator(); + } + } + } +} \ No newline at end of file diff --git a/src/xunit.runner.android/Elements/ActionElement.cs b/src/xunit.runner.android/Elements/ActionElement.cs new file mode 100644 index 0000000..ff0a141 --- /dev/null +++ b/src/xunit.runner.android/Elements/ActionElement.cs @@ -0,0 +1,47 @@ +// +// Copyright 2011-2012 Xamarin Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +using Android.Content; +using Android.Views; + +using MonoDroid.Dialog; + +namespace Xunit.Runners.UI +{ + + internal class ActionElement : StringElement { + + Action action; + + public ActionElement (string name, Action action) : base (name) + { + this.action = action; + Value = "..."; // hint some action will take place + } + + public override View GetView (Context context, View convertView, ViewGroup parent) + { + View view = base.GetView (context, convertView, parent); + view.Click += delegate { + // FIXME: show activity/progress + action (); + }; + return view; + } + } +} \ No newline at end of file diff --git a/src/xunit.runner.android/Elements/ActivityElement.cs b/src/xunit.runner.android/Elements/ActivityElement.cs new file mode 100644 index 0000000..843bf32 --- /dev/null +++ b/src/xunit.runner.android/Elements/ActivityElement.cs @@ -0,0 +1,50 @@ +// +// Copyright 2011-2012 Xamarin Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +using Android.Content; +using Android.Views; + +using MonoDroid.Dialog; + +namespace Xunit.Runners.UI +{ + + internal class ActivityElement : StringElement + { + + Type activity; + + public ActivityElement (string name, Type type) : base (name) + { + activity = type; + Value = ">"; // hint there's something more to show + } + + public override View GetView (Context context, View convertView, ViewGroup parent) + { + View view = base.GetView (context, convertView, parent); + view.Click += delegate { + Intent intent = new Intent (context, activity); + intent.AddFlags (ActivityFlags.NewTask); + context.StartActivity (intent); + }; + return view; + } + } +} + diff --git a/src/xunit.runner.android/Elements/FormattedElement.cs b/src/xunit.runner.android/Elements/FormattedElement.cs new file mode 100644 index 0000000..5b6f42f --- /dev/null +++ b/src/xunit.runner.android/Elements/FormattedElement.cs @@ -0,0 +1,87 @@ +// +// Copyright 2011-2012 Xamarin Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +using Android.Content; +using Android.Views; +using Android.Widget; + +using MonoDroid.Dialog; + +namespace Xunit.Runners.UI +{ + + class FormattedElement : StringElement { + + private new TextView _caption; + private new TextView _text; + + private string captionText; + + + public FormattedElement (string caption) : base (caption) + { + } + + public string Indicator { + get; set; + } + + public override View GetView (Context context, View convertView, ViewGroup parent) + { + var view = new RelativeLayout(context); + + var parms = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WrapContent, + ViewGroup.LayoutParams.WrapContent); + parms.SetMargins(5, 3, 5, 0); + parms.AddRule(LayoutRules.AlignParentLeft); + + _caption = new TextView (context); + if (string.IsNullOrWhiteSpace(captionText)) + SetCaption(Caption); + else + { + SetCaption(captionText); + captionText = null; + } + view.AddView(_caption, parms); + + if (!String.IsNullOrWhiteSpace (Indicator)) { + var tparms = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WrapContent, + ViewGroup.LayoutParams.WrapContent); + tparms.SetMargins(5, 3, 5, 5); + tparms.AddRule(LayoutRules.CenterVertical); + tparms.AddRule(LayoutRules.AlignParentRight); + + _text = new TextView (context) { + Text = Indicator, + TextSize = 22f + }; + view.AddView(_text, tparms); + } + return view; + } + + public void SetCaption(string html) + { + if (_caption != null) + _caption.SetText(Android.Text.Html.FromHtml(html), TextView.BufferType.Spannable); + + captionText = html; + } + } +} diff --git a/src/xunit.runner.android/Elements/FormattedSection.cs b/src/xunit.runner.android/Elements/FormattedSection.cs new file mode 100644 index 0000000..1629d42 --- /dev/null +++ b/src/xunit.runner.android/Elements/FormattedSection.cs @@ -0,0 +1,54 @@ +// +// Copyright 2011-2012 Xamarin Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +using Android.App; +using Android.Content; +using Android.OS; +using Android.Runtime; +using Android.Views; +using Android.Widget; + +using MonoDroid.Dialog; + +namespace Xunit.Runners.UI +{ + + // can't really name it HtmlSection wrt HtmlElement ;-) + internal class FormattedSection : Section { + + public FormattedSection (string html) + : base (html) + { + } + + public override View GetView (Context context, View convertView, ViewGroup parent) + { + TextView tv = new TextView (context); + tv.TextSize = 20f; + tv.SetText (Android.Text.Html.FromHtml (Caption), TextView.BufferType.Spannable); + + var parms = new RelativeLayout.LayoutParams (ViewGroup.LayoutParams.WrapContent, ViewGroup.LayoutParams.WrapContent); + parms.AddRule (LayoutRules.CenterHorizontal); + + RelativeLayout view = new RelativeLayout (context, null, Android.Resource.Attribute.ListSeparatorTextViewStyle); + view.AddView (tv, parms); + return view; + } + } +} + diff --git a/src/xunit.runner.android/Elements/TestCaseElement.cs b/src/xunit.runner.android/Elements/TestCaseElement.cs new file mode 100644 index 0000000..cec417f --- /dev/null +++ b/src/xunit.runner.android/Elements/TestCaseElement.cs @@ -0,0 +1,129 @@ +// +// Copyright 2011-2012 Xamarin Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Android.Content; +using Android.Views; +using Xunit.Abstractions; + + +namespace Xunit.Runners.UI +{ + internal class TestCaseElement : TestElement + { + public TestCaseElement(MonoTestCase testCase, AndroidRunner runner) + : base(runner) + { + if (testCase == null) throw new ArgumentNullException("testCase"); + + TestCase = testCase; + + MonoTestResult result; + Runner.Results.TryGetValue(testCase.UniqueName, out result); + + if (testCase.Result == TestState.NotRun) + Indicator = "..."; // hint there's more + + Refresh(); + + testCase.TestCaseUpdated += OnTestCaseUpdated; + } + + private void OnTestCaseUpdated(object sender, EventArgs e) + { + Refresh(); + } + + + public MonoTestCase TestCase { get; private set; } + + + + public override TestState Result + { + get { return TestCase.Result; } + } + + protected override string GetCaption() + { + + + + if (TestCase.Result == TestState.Skipped) + { + var val = ((ITestSkipped)TestCase.TestResult.TestResultMessage).Reason; + return string.Format("{0}
{1}: {2}", + TestCase.DisplayName, TestState.Skipped, val); + } + else if (TestCase.Result == TestState.Passed) + { + Indicator = null; + return string.Format("{0}
Success! {1} ms", TestCase.DisplayName, TestCase.TestResult.Duration.TotalMilliseconds); + } + else if (TestCase.Result == TestState.Failed) + { + var val = TestCase.TestResult.ErrorMessage; + return string.Format("{0}
{1}", TestCase.DisplayName, val); + } + else + { + // Assert.Ignore falls into this + var val = TestCase.TestResult.ErrorMessage; + return string.Format("{0}
{1}", TestCase.DisplayName, val); + } + } + + + public async Task Run() + { + if (TestCase.Result == TestState.NotRun) + { + await Runner.Run(TestCase); + } + } + + public override View GetView(Context context, View convertView, ViewGroup parent) + { + Refresh(); + var view = base.GetView(context, convertView, parent); + view.Click += async delegate + { + await Run(); + + if (Result != TestState.Passed) + { + var intent = new Intent(context, typeof(TestResultActivity)); + intent.PutExtra("TestCase", TestCase.UniqueName); + intent.AddFlags(ActivityFlags.NewTask); + context.StartActivity(intent); + } + }; + return view; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + TestCase.TestCaseUpdated -= OnTestCaseUpdated; + } + base.Dispose(disposing); + } + } +} \ No newline at end of file diff --git a/src/xunit.runner.android/Elements/TestElement.cs b/src/xunit.runner.android/Elements/TestElement.cs new file mode 100644 index 0000000..7e377eb --- /dev/null +++ b/src/xunit.runner.android/Elements/TestElement.cs @@ -0,0 +1,53 @@ +// +// Copyright 2011-2012 Xamarin Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using Android.Graphics; + +namespace Xunit.Runners.UI +{ + + internal abstract class TestElement : FormattedElement + { + + protected TestElement(AndroidRunner runner) + : base(String.Empty) + { + if (runner == null) throw new ArgumentNullException("runner"); + + Runner = runner; + } + + + + protected virtual void OptionsChanged() + { + + } + + public abstract TestState Result { get; } + + protected abstract string GetCaption(); + + public void Refresh() + { + var caption = GetCaption(); + SetCaption(caption); + } + + protected AndroidRunner Runner { get; private set; } + } +} \ No newline at end of file diff --git a/src/xunit.runner.android/Elements/TestSuiteElement.cs b/src/xunit.runner.android/Elements/TestSuiteElement.cs new file mode 100644 index 0000000..ccbdabb --- /dev/null +++ b/src/xunit.runner.android/Elements/TestSuiteElement.cs @@ -0,0 +1,124 @@ +// +// Copyright 2011-2012 Xamarin Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Android.Content; +using Android.Views; + + +namespace Xunit.Runners.UI +{ + internal class TestSuiteElement : TestElement + { + private readonly string sourceName; + private readonly IEnumerable testCases = Enumerable.Empty(); + private TestState result = TestState.NotRun; + + public IEnumerable TestCases { get { return testCases; } } + + public TestSuiteElement(string sourceName, IEnumerable testCases, AndroidRunner runner) + : base(runner) + { + this.sourceName = sourceName; + this.testCases = testCases; + + if (testCases.Any()) + Indicator = ">"; // hint there's more + + Refresh(); + } + + + protected override string GetCaption() + { + var count = testCases.Count(); + var caption = String.Format("{0}
", sourceName); + if (count == 0) + { + caption += "no test was found inside this suite"; + } + else + { + var outcomes = testCases.GroupBy(r => r.Result); + + var results = outcomes.ToDictionary(k => k.Key, v => v.Count()); + + int positive; + results.TryGetValue(TestState.Passed, out positive); + + int failure; + results.TryGetValue(TestState.Failed, out failure); + + int skipped; + results.TryGetValue(TestState.Skipped, out skipped); + + int notRun; + results.TryGetValue(TestState.NotRun, out notRun); + + // No failures and all run + if (failure == 0 && notRun == 0) + { + caption += string.Format("Success! {0} test{1}", + positive, positive == 1 ? string.Empty : "s"); + + result = TestState.Passed; + } + else if (failure > 0 || (notRun > 0 && notRun < count)) + { + // we either have failures or some of the tests are not run + caption += String.Format("{0} success, {1} failure{2}, {3} skip{4}, {5} not run", + positive, failure, failure > 1 ? "s" : String.Empty, + skipped, skipped > 1 ? "s" : String.Empty, + notRun); + + result = TestState.Failed; + } + else if (Result == TestState.NotRun) + { + caption += String.Format("{0} test case{1}, {2}", + count, count == 1 ? String.Empty : "s", Result); + } + } + return caption; + } + + public override View GetView(Context context, View convertView, ViewGroup parent) + { + Refresh(); + var view = base.GetView(context, convertView, parent); + // if there are test cases inside this suite then create an activity to show them + if (testCases.Any()) + { + view.Click += delegate + { + var intent = new Intent(context, typeof(TestSuiteActivity)); + intent.PutExtra("TestSuite", sourceName); + intent.AddFlags(ActivityFlags.NewTask); + context.StartActivity(intent); + }; + } + return view; + } + + public override TestState Result + { + get { return result; } + } + } +} \ No newline at end of file diff --git a/src/xunit.runner.android/MonoTouch.Dialog/BindingContext.cs b/src/xunit.runner.android/MonoTouch.Dialog/BindingContext.cs new file mode 100644 index 0000000..44fa301 --- /dev/null +++ b/src/xunit.runner.android/MonoTouch.Dialog/BindingContext.cs @@ -0,0 +1,401 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using Android.Content; +using Android.Widget; + +namespace MonoDroid.Dialog +{ + internal class BindingContext : IDisposable + { + public RootElement Root; + Dictionary mappings; + private Context _context; + + class MemberAndInstance + { + public MemberAndInstance(MemberInfo mi, object o) + { + Member = mi; + Obj = o; + } + public MemberInfo Member; + public object Obj; + } + + static object GetValue(MemberInfo mi, object o) + { + var fi = mi as FieldInfo; + if (fi != null) + return fi.GetValue(o); + var pi = mi as PropertyInfo; + + var getMethod = pi.GetGetMethod(); + return getMethod.Invoke(o, new object[0]); + } + + static void SetValue(MemberInfo mi, object o, object val) + { + var fi = mi as FieldInfo; + if (fi != null) + { + fi.SetValue(o, val); + return; + } + var pi = mi as PropertyInfo; + var setMethod = pi.GetSetMethod(); + setMethod.Invoke(o, new object[] { val }); + } + + static string MakeCaption(string name) + { + var sb = new StringBuilder(name.Length); + bool nextUp = true; + + foreach (char c in name) + { + if (nextUp) + { + sb.Append(Char.ToUpper(c)); + nextUp = false; + } + else + { + if (c == '_') + { + sb.Append(' '); + continue; + } + if (Char.IsUpper(c)) + sb.Append(' '); + sb.Append(c); + } + } + return sb.ToString(); + } + + // Returns the type for fields and properties and null for everything else + static Type GetTypeForMember(MemberInfo mi) + { + if (mi is FieldInfo) + return ((FieldInfo)mi).FieldType; + else if (mi is PropertyInfo) + return ((PropertyInfo)mi).PropertyType; + return null; + } + + public BindingContext(Context context, object callbacks, object o, string title) + { + _context = context; + + if (o == null) + throw new ArgumentNullException("o"); + + mappings = new Dictionary(); + + Root = new RootElement(title); + Populate(callbacks, o, Root); + } + + void Populate(object callbacks, object o, RootElement root) + { + MemberInfo last_radio_index = null; + var members = o.GetType().GetMembers(BindingFlags.DeclaredOnly | BindingFlags.Public | + BindingFlags.NonPublic | BindingFlags.Instance); + + Section section = null; + + foreach (var mi in members) + { + Type mType = GetTypeForMember(mi); + + if (mType == null) + continue; + + string caption = null; + object[] attrs = mi.GetCustomAttributes(false); + bool skip = false; + foreach (var attr in attrs) + { + if (attr is SkipAttribute) + skip = true; + if (attr is CaptionAttribute) + caption = ((CaptionAttribute)attr).Caption; + else if (attr is SectionAttribute) + { + if (section != null) + root.Add(section); + var sa = attr as SectionAttribute; + section = new Section(sa.Caption, sa.Footer); + } + } + if (skip) + continue; + + if (caption == null) + caption = MakeCaption(mi.Name); + + if (section == null) + section = new Section(); + + Element element = null; + if (mType == typeof(string)) + { + PasswordAttribute pa = null; + AlignmentAttribute align = null; + EntryAttribute ea = null; + object html = null; + EventHandler invoke = null; + bool multi = false; + + foreach (object attr in attrs) + { + if (attr is PasswordAttribute) + pa = attr as PasswordAttribute; + else if (attr is EntryAttribute) + ea = attr as EntryAttribute; + else if (attr is MultilineAttribute) + multi = true; + else if (attr is HtmlAttribute) + html = attr; + else if (attr is AlignmentAttribute) + align = attr as AlignmentAttribute; + + if (attr is OnTapAttribute) + { + string mname = ((OnTapAttribute)attr).Method; + + if (callbacks == null) + { + throw new Exception("Your class contains [OnTap] attributes, but you passed a null object for `context' in the constructor"); + } + + var method = callbacks.GetType().GetMethod(mname); + if (method == null) + throw new Exception("Did not find method " + mname); + invoke = delegate + { + method.Invoke(method.IsStatic ? null : callbacks, new object[0]); + }; + } + } + + string value = (string)GetValue(mi, o); + if (pa != null) + element = new EntryElement(caption, value) { Hint = pa.Placeholder, Password = true }; + else if (ea != null) + element = new EntryElement(caption, value) { Hint = ea.Placeholder }; + else if (multi) + element = new MultilineElement(caption, value); + else if (html != null) + element = new HtmlElement(caption, value); + else + { + var selement = new StringElement(caption, value); + element = selement; + + if (align != null) + selement.Alignment = align.Alignment; + } + + if (invoke != null) + ((StringElement)element).Click = invoke; + } + else if (mType == typeof(float)) + { + var floatElement = new FloatElement(null, null, (int)GetValue(mi, o)); + floatElement.Caption = caption; + element = floatElement; + + foreach (object attr in attrs) + { + if (attr is RangeAttribute) + { + var ra = attr as RangeAttribute; + floatElement.MinValue = ra.Low; + floatElement.MaxValue = ra.High; + floatElement.ShowCaption = ra.ShowCaption; + } + } + } + else if (mType == typeof(bool)) + { + bool checkbox = false; + foreach (object attr in attrs) + { + if (attr is CheckboxAttribute) + checkbox = true; + } + + if (checkbox) + element = new CheckboxElement(caption, (bool)GetValue(mi, o)); + else + element = new BooleanElement(caption, (bool)GetValue(mi, o)); + } + else if (mType == typeof(DateTime)) + { + var dateTime = (DateTime)GetValue(mi, o); + bool asDate = false, asTime = false; + + foreach (object attr in attrs) + { + if (attr is DateAttribute) + asDate = true; + else if (attr is TimeAttribute) + asTime = true; + } + + if (asDate) + element = new DateElement(caption, dateTime); + else if (asTime) + element = new TimeElement(caption, dateTime); + else + element = new DateTimeElement(caption, dateTime); + } + else if (mType.IsEnum) + { + var csection = new Section(); + ulong evalue = Convert.ToUInt64(GetValue(mi, o), null); + int idx = 0; + int selected = 0; + + foreach (var fi in mType.GetFields(BindingFlags.Public | BindingFlags.Static)) + { + ulong v = Convert.ToUInt64(GetValue(fi, null)); + + if (v == evalue) + selected = idx; + + csection.Add(new RadioElement(MakeCaption(fi.Name))); + idx++; + } + + element = new RootElement(caption, new RadioGroup(null, selected)) { csection }; + } + else if (mType == typeof(ImageView)) + { + element = new ImageElement(null); // (ImageView)GetValue(mi, o)); + } + else if (typeof(System.Collections.IEnumerable).IsAssignableFrom(mType)) + { + var csection = new Section(); + int count = 0; + + if (last_radio_index == null) + throw new Exception("IEnumerable found, but no previous int found"); + foreach (var e in (IEnumerable)GetValue(mi, o)) + { + csection.Add(new RadioElement(e.ToString())); + count++; + } + int selected = (int)GetValue(last_radio_index, o); + if (selected >= count || selected < 0) + selected = 0; + element = new RootElement(caption, new MemberRadioGroup(null, selected, last_radio_index)) { csection }; + last_radio_index = null; + } + else if (typeof(int) == mType) + { + foreach (object attr in attrs) + { + if (attr is RadioSelectionAttribute) + { + last_radio_index = mi; + break; + } + } + } + else + { + var nested = GetValue(mi, o); + if (nested != null) + { + var newRoot = new RootElement(caption); + Populate(callbacks, nested, newRoot); + element = newRoot; + } + } + + if (element == null) + continue; + + section.Add(element); + mappings[element] = new MemberAndInstance(mi, o); + } + root.Add(section); + } + + class MemberRadioGroup : RadioGroup + { + public MemberInfo mi; + + public MemberRadioGroup(string key, int selected, MemberInfo mi) + : base(key, selected) + { + this.mi = mi; + } + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + foreach (var element in mappings.Keys) + { + element.Dispose(); + } + mappings = null; + } + } + + public void Fetch() + { + foreach (var dk in mappings) + { + Element element = dk.Key; + MemberInfo mi = dk.Value.Member; + object obj = dk.Value.Obj; + + if (element is DateTimeElement) + SetValue(mi, obj, ((DateTimeElement)element).DateValue); + else if (element is FloatElement) + SetValue(mi, obj, ((FloatElement)element).Value); + else if (element is BooleanElement) + SetValue(mi, obj, ((BooleanElement)element).Value); + else if (element is CheckboxElement) + SetValue(mi, obj, ((CheckboxElement)element).Value); + else if (element is EntryElement) + { + var entry = (EntryElement)element; + // TODO: entry.FetchValue(); + SetValue(mi, obj, entry.Value); + } + else if (element is ImageElement) + SetValue(mi, obj, ((ImageElement)element).Value); + else if (element is RootElement) + { + var re = element as RootElement; + if (re._group as MemberRadioGroup != null) + { + var group = re._group as MemberRadioGroup; + SetValue(group.mi, obj, re.RadioSelected); + } + else if (re._group as RadioGroup != null) + { + var mType = GetTypeForMember(mi); + var fi = mType.GetFields(BindingFlags.Public | BindingFlags.Static)[re.RadioSelected]; + + SetValue(mi, obj, fi.GetValue(null)); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/xunit.runner.android/MonoTouch.Dialog/BooleanElement.cs b/src/xunit.runner.android/MonoTouch.Dialog/BooleanElement.cs new file mode 100644 index 0000000..1cab785 --- /dev/null +++ b/src/xunit.runner.android/MonoTouch.Dialog/BooleanElement.cs @@ -0,0 +1,96 @@ +using System; +using Android.Content; +using Android.Views; +using Android.Widget; + +namespace MonoDroid.Dialog +{ + internal abstract class BoolElement : Element + { + private bool _val; + + public bool Value + { + get { return _val; } + set + { + if (_val != value) + { + _val = value; + if (ValueChanged != null) + ValueChanged(this, EventArgs.Empty); + } + } + } + + public event EventHandler ValueChanged; + + public BoolElement(string caption, bool value) : base(caption) + { + _val = value; + } + + public BoolElement(string caption, bool value, int layoutId) + : base(caption, layoutId) + { + _val = value; + } + + public override string Summary() + { + return _val ? "On" : "Off"; + } + } + + /// + /// Used to display toggle button on the screen. + /// + internal class BooleanElement : BoolElement, CompoundButton.IOnCheckedChangeListener + { + private ToggleButton _toggleButton; + private TextView _caption; + private TextView _subCaption; + + public BooleanElement(string caption, bool value) + : base(caption, value, (int) DroidResources.ElementLayout.dialog_onofffieldright) + { + } + + public BooleanElement(string caption, bool value, int layoutId) + : base(caption, value, layoutId) + { + } + + public override View GetView(Context context, View convertView, ViewGroup parent) + { + View toggleButtonView; + View view = DroidResources.LoadBooleanElementLayout(context, convertView, parent, LayoutId, out _caption, out _subCaption, out toggleButtonView); + + if (view != null) + { + _caption.Text = Caption; + _toggleButton = toggleButtonView as ToggleButton; + _toggleButton.SetOnCheckedChangeListener(null); + _toggleButton.Checked = Value; + _toggleButton.SetOnCheckedChangeListener(this); + } + return view; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + //_toggleButton.Dispose(); + _toggleButton = null; + //_caption.Dispose(); + _caption = null; + } + } + + public void OnCheckedChanged(CompoundButton buttonView, bool isChecked) + { + this.Value = isChecked; + } + } +} \ No newline at end of file diff --git a/src/xunit.runner.android/MonoTouch.Dialog/ButtonElement.cs b/src/xunit.runner.android/MonoTouch.Dialog/ButtonElement.cs new file mode 100644 index 0000000..a27057d --- /dev/null +++ b/src/xunit.runner.android/MonoTouch.Dialog/ButtonElement.cs @@ -0,0 +1,35 @@ +using System; + +using Android.Content; +using Android.Views; +using Android.Widget; + +namespace MonoDroid.Dialog +{ + internal class ButtonElement : StringElement + { + public ButtonElement (string caption, EventHandler tapped) + : base(caption, (int)DroidResources.ElementLayout.dialog_button) + { + this.Click = tapped; + } + + public override View GetView (Context context, View convertView, ViewGroup parent) + { + Button button; + var view = DroidResources.LoadButtonLayout (context, convertView, parent, LayoutId, out button); + if (view != null) { + button.Text = Caption; + if (Click != null) + button.Click += Click; + } + + return view; + } + + public override string Summary () + { + return Caption; + } + } +} \ No newline at end of file diff --git a/src/xunit.runner.android/MonoTouch.Dialog/CheckboxElement.cs b/src/xunit.runner.android/MonoTouch.Dialog/CheckboxElement.cs new file mode 100644 index 0000000..1ba975f --- /dev/null +++ b/src/xunit.runner.android/MonoTouch.Dialog/CheckboxElement.cs @@ -0,0 +1,110 @@ +using System; +using Android.Content; +using Android.Views; +using Android.Widget; + +namespace MonoDroid.Dialog +{ + internal class CheckboxElement : Element, CompoundButton.IOnCheckedChangeListener + { + public bool Value + { + get { return _val; } + set + { + bool emit = _val != value; + _val = value; + if(_checkbox != null && _checkbox.Checked != _val) + _checkbox.Checked = _val; + else if (emit && ValueChanged != null) + ValueChanged(this, EventArgs.Empty); + } + } + private bool _val; + + public string SubCaption + { + get + { + return subCap; + } + set + { + subCap = value; + } + } + private string subCap; + + public bool ReadOnly + { + get; + set; + } + + public event EventHandler ValueChanged; + + private CheckBox _checkbox; + private TextView _caption; + private TextView _subCaption; + + public string Group; + + public CheckboxElement(string caption) + : base(caption, (int)DroidResources.ElementLayout.dialog_boolfieldright) + { + Value = false; + } + + public CheckboxElement(string caption, bool value) + : base(caption, (int)DroidResources.ElementLayout.dialog_boolfieldright) + { + Value = value; + } + + public CheckboxElement(string caption, bool value, string subCaption, string group) + : base(caption, (int)DroidResources.ElementLayout.dialog_boolfieldsubright) + { + Value = value; + Group = group; + SubCaption = subCaption; + } + + public CheckboxElement(string caption, bool value, string group) + : base(caption, (int)DroidResources.ElementLayout.dialog_boolfieldright) + { + Value = value; + Group = group; + } + + public CheckboxElement(string caption, bool value, string group, int layoutId) + : base(caption, layoutId) + { + Value = value; + Group = group; + } + + public override View GetView(Context context, View convertView, ViewGroup parent) + { + View checkboxView; + View view = DroidResources.LoadBooleanElementLayout(context, convertView, parent, LayoutId, out _caption, out _subCaption, out checkboxView); + if (view != null) + { + _caption.Text = Caption; + + _checkbox = checkboxView as CheckBox; + _checkbox.SetOnCheckedChangeListener(null); + _checkbox.Checked = Value; + _checkbox.Clickable = !ReadOnly; + + if (_subCaption != null) + _subCaption.Text = SubCaption; + } + return view; + } + + public void OnCheckedChanged(CompoundButton buttonView, bool isChecked) + { + this.Value = isChecked; + } + } +} \ No newline at end of file diff --git a/src/xunit.runner.android/MonoTouch.Dialog/DateTimeElement.cs b/src/xunit.runner.android/MonoTouch.Dialog/DateTimeElement.cs new file mode 100644 index 0000000..79258e2 --- /dev/null +++ b/src/xunit.runner.android/MonoTouch.Dialog/DateTimeElement.cs @@ -0,0 +1,111 @@ +using System; +using Android.App; +using Android.Content; + +namespace MonoDroid.Dialog +{ + internal class DateTimeElement : StringElement + { + public DateTime DateValue + { + get { return DateTime.Parse(Value); } + set { Value = Format(value); } + } + + public DateTimeElement(string caption, DateTime date) + : base(caption) + { + DateValue = date; + } + + public DateTimeElement(string caption, DateTime date, int layoutId) + : base(caption, layoutId) + { + DateValue = date; + } + + public virtual string Format(DateTime dt) + { + return dt.ToShortDateString() + " " + dt.ToShortTimeString(); + } + } + + internal class DateElement : DateTimeElement + { + public DateElement(string caption, DateTime date) + : base(caption, date) + { + this.Click = delegate { EditDate(); }; + } + + public DateElement(string caption, DateTime date, int layoutId) + : base(caption, date, layoutId) + { + this.Click = delegate { EditDate(); }; + } + + public override string Format(DateTime dt) + { + return dt.ToShortDateString(); + } + + // the event received when the user "sets" the date in the dialog + void OnDateSet(object sender, DatePickerDialog.DateSetEventArgs e) + { + DateTime current = DateValue; + DateValue = new DateTime(e.Date.Year, e.Date.Month, e.Date.Day, current.Hour, current.Minute, 0); + } + + private void EditDate() + { + Context context = GetContext(); + if (context == null) + { + Android.Util.Log.Warn("DateElement", "No Context for Edit"); + return; + } + DateTime val = DateValue; + new DatePickerDialog(context, OnDateSet, val.Year, val.Month - 1, val.Day).Show(); + } + } + + internal class TimeElement : DateTimeElement + { + public TimeElement(string caption, DateTime date) + : base(caption, date) + { + this.Click = delegate { EditDate(); }; + } + + public TimeElement(string caption, DateTime date, int layoutId) + : base(caption, date, layoutId) + { + this.Click = delegate { EditDate(); }; + } + + public override string Format(DateTime dt) + { + return dt.ToShortTimeString(); + } + + // the event received when the user "sets" the date in the dialog + void OnDateSet(object sender, TimePickerDialog.TimeSetEventArgs e) + { + DateTime current = DateValue; + DateValue = new DateTime(current.Year, current.Month, current.Day, e.HourOfDay, e.Minute, 0); + } + + private void EditDate() + { + Context context = GetContext(); + if (context == null) + { + Android.Util.Log.Warn("TimeElement", "No Context for Edit"); + return; + } + DateTime val = DateValue; + // TODO: get the current time setting for thge 24 hour clock + new TimePickerDialog(context, OnDateSet, val.Hour, val.Minute, false).Show(); + } + } +} \ No newline at end of file diff --git a/src/xunit.runner.android/MonoTouch.Dialog/DialogActivity.cs b/src/xunit.runner.android/MonoTouch.Dialog/DialogActivity.cs new file mode 100644 index 0000000..6aa9f78 --- /dev/null +++ b/src/xunit.runner.android/MonoTouch.Dialog/DialogActivity.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using Android.App; +using Android.OS; + +namespace MonoDroid.Dialog +{ + internal class DialogInstanceData : Java.Lang.Object + { + public DialogInstanceData() + { + _dialogState = new Dictionary(); + } + + private Dictionary _dialogState; + } + + internal class DialogActivity : ListActivity + { + public RootElement Root { get; set; } + private DialogHelper Dialog { get; set; } + + protected override void OnCreate(Bundle savedInstanceState) + { + base.OnCreate(savedInstanceState); + Dialog = new DialogHelper(this, this.ListView, this.Root); + + if (this.LastNonConfigurationInstance != null) + { + // apply value changes that are saved + } + } + + public override Java.Lang.Object OnRetainNonConfigurationInstance() + { + return null; + } + } +} \ No newline at end of file diff --git a/src/xunit.runner.android/MonoTouch.Dialog/DialogAdapter.cs b/src/xunit.runner.android/MonoTouch.Dialog/DialogAdapter.cs new file mode 100644 index 0000000..eaf3db7 --- /dev/null +++ b/src/xunit.runner.android/MonoTouch.Dialog/DialogAdapter.cs @@ -0,0 +1,150 @@ +using Android.Content; +using Android.Views; +using Android.Widget; + +namespace MonoDroid.Dialog +{ + internal class DialogAdapter : BaseAdapter
+ { + const int TYPE_SECTION_HEADER = 0; + + Context context; + LayoutInflater inflater; + + public DialogAdapter(Context context, RootElement root) + { + this.context = context; + this.inflater = LayoutInflater.From(context); + this.Root = root; + } + + public RootElement Root { get; set; } + + public override bool IsEnabled(int position) + { + // start counting from here + int typeOffset = TYPE_SECTION_HEADER + 1; + + foreach (var s in Root.Sections) + { + if (position == 0) + return false; + + int size = s.Adapter.Count + 1; + + if (position < size) + return true; + + position -= size; + typeOffset += s.Adapter.ViewTypeCount; + } + + return false; + } + + public override int Count + { + get + { + int count = 0; + + //Get each adapter's count + 1 for the header + foreach (var s in Root.Sections) + count += s.Adapter.Count + 1; + + return count; + } + } + + public override int ViewTypeCount + { + get + { + // ViewTypeCount is the same as Count for these, + // there are as many ViewTypes as Views as everyone is unique! + return Count; + } + } + + public Element ElementAtIndex(int position) + { + int sectionIndex = 0; + foreach (var s in Root.Sections) + { + if (position == 0) + return this.Root.Sections[sectionIndex]; + + // note: plus one for the section header view + int size = s.Adapter.Count + 1; + if (position < size) + return this.Root.Sections[sectionIndex].Elements[position - 1]; + + position -= size; + sectionIndex++; + } + + return null; + } + + public override Section this[int position] + { + get { return this.Root.Sections[position]; } + } + + public override bool AreAllItemsEnabled() + { + return false; + } + + public override int GetItemViewType(int position) + { + // start counting from here + int typeOffset = TYPE_SECTION_HEADER + 1; + + foreach (var s in Root.Sections) + { + if (position == 0) + return (TYPE_SECTION_HEADER); + + int size = s.Adapter.Count + 1; + + if (position < size) + return (typeOffset + s.Adapter.GetItemViewType(position - 1)); + + position -= size; + typeOffset += s.Adapter.ViewTypeCount; + } + + return -1; + } + + public override long GetItemId(int position) + { + return position; + } + + public override View GetView(int position, View convertView, ViewGroup parent) + { + int sectionIndex = 0; + + foreach (var s in Root.Sections) + { + if (s.Adapter.Context == null) + s.Adapter.Context = this.context; + + if (position == 0) + return s.GetView(context, convertView, parent); + + int size = s.Adapter.Count + 1; + + if (position < size) + return (s.Adapter.GetView(position - 1, convertView, parent)); + + position -= size; + sectionIndex++; + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/xunit.runner.android/MonoTouch.Dialog/DialogHelper.cs b/src/xunit.runner.android/MonoTouch.Dialog/DialogHelper.cs new file mode 100644 index 0000000..8b8ea62 --- /dev/null +++ b/src/xunit.runner.android/MonoTouch.Dialog/DialogHelper.cs @@ -0,0 +1,44 @@ +using System; +using Android.Content; +using Android.Widget; + +namespace MonoDroid.Dialog +{ + internal class DialogHelper + { + private Context context; + private RootElement formLayer; + + //public event Action ElementClick; + //public event Action ElementLongClick; + + public RootElement Root { get; set; } + + private DialogAdapter DialogAdapter { get; set; } + + public DialogHelper(Context context, ListView dialogView, RootElement root) + { + this.Root = root; + this.Root.Context = context; + + dialogView.Adapter = this.DialogAdapter = new DialogAdapter(context, this.Root); + dialogView.ItemClick += new EventHandler(ListView_ItemClick); + //dialogView.ItemLongClick += new EventHandler(ListView_ItemLongClick); + dialogView.Tag = root; + } + + void ListView_ItemLongClick(object sender, ItemEventArgs e) + { + var elem = this.DialogAdapter.ElementAtIndex(e.Position); + if (elem != null && elem.LongClick != null) + elem.LongClick(sender, e); + } + + void ListView_ItemClick(object sender, AdapterView.ItemClickEventArgs e) + { + var elem = this.DialogAdapter.ElementAtIndex(e.Position); + if (elem != null && elem.Click != null) + elem.Click(sender, e); + } + } +} \ No newline at end of file diff --git a/src/xunit.runner.android/MonoTouch.Dialog/DroidResources.cs b/src/xunit.runner.android/MonoTouch.Dialog/DroidResources.cs new file mode 100644 index 0000000..bdd2be5 --- /dev/null +++ b/src/xunit.runner.android/MonoTouch.Dialog/DroidResources.cs @@ -0,0 +1,211 @@ +using System; +using System.Collections.Generic; +using Android.Content; +using Android.Util; +using Android.Views; +using Android.Widget; + +namespace MonoDroid.Dialog +{ + internal static class DroidResources + { + public enum ElementLayout: int + { + dialog_boolfieldleft, + dialog_boolfieldright, + dialog_boolfieldsubleft, + dialog_boolfieldsubright, + + dialog_button, + dialog_datefield, + dialog_fieldsetlabel, + dialog_labelfieldbelow, + dialog_labelfieldright, + dialog_onofffieldright, + dialog_panel, + dialog_root, + dialog_selectlist, + dialog_selectlistfield, + dialog_textarea, + + dialog_floatimage, + + dialog_textfieldbelow, + dialog_textfieldright, + } + + public static View LoadFloatElementLayout(Context context, View convertView, ViewGroup parent, int layoutId, out TextView label, out SeekBar slider, out ImageView left, out ImageView right) + { + View layout = convertView ?? LoadLayout(context, parent, layoutId); + if (layout != null) + { + label = layout.FindViewById(context.Resources.GetIdentifier("dialog_LabelField", "id", context.PackageName)); + slider = layout.FindViewById(context.Resources.GetIdentifier("dialog_SliderField", "id", context.PackageName)); + left = layout.FindViewById(context.Resources.GetIdentifier("dialog_ImageLeft", "id", context.PackageName)); + right = layout.FindViewById(context.Resources.GetIdentifier("dialog_ImageRight", "id", context.PackageName)); + } + else + { + label = null; + slider = null; + left = right = null; + } + return layout; + } + + + private static View LoadLayout(Context context, ViewGroup parent, int layoutId) + { + try + { + LayoutInflater inflater = LayoutInflater.FromContext(context); + if (_resourceMap.ContainsKey((ElementLayout)layoutId)) + { + string layoutName = _resourceMap[(ElementLayout)layoutId]; + int layoutIndex = context.Resources.GetIdentifier(layoutName, "layout", context.PackageName); + return inflater.Inflate(layoutIndex, parent, false); + } + else + { + // TODO: figure out what context to use to get this right, currently doesn't inflate application resources + return inflater.Inflate(layoutId, parent, false); + } + } + catch (InflateException ex) + { + Log.Error("MDD", "Inflate failed: " + ex.Cause.Message); + } + catch (Exception ex) + { + Log.Error("MDD", "LoadLayout failed: " + ex.Message); + } + return null; + } + + public static View LoadStringElementLayout(Context context, View convertView, ViewGroup parent, int layoutId, out TextView label, out TextView value) + { + // We are not recycling views here because Android.Dialog is leaking anon.Click handlers + //View layout = convertView ?? LoadLayout(context, parent, layoutId); + View layout = LoadLayout(context, parent, layoutId); + if (layout != null) + { + label = layout.FindViewById(context.Resources.GetIdentifier("dialog_LabelField", "id", context.PackageName)); + value = layout.FindViewById(context.Resources.GetIdentifier("dialog_ValueField", "id", context.PackageName)); + } + else + { + label = null; + value = null; + } + return layout; + } + + public static View LoadButtonLayout(Context context, View convertView, ViewGroup parent, int layoutId, out Button button) + { + View layout = LoadLayout(context, parent, layoutId); + if (layout != null) + { + button = layout.FindViewById