Initial files for xunit-based tests
authorIgor Kulaychuk <i.kulaychuk@samsung.com>
Mon, 25 Sep 2017 13:31:19 +0000 (16:31 +0300)
committerIgor Kulaychuk <i.kulaychuk@samsung.com>
Tue, 27 Mar 2018 18:44:15 +0000 (21:44 +0300)
netcoredbg.sln [new file with mode: 0644]
tests/README.md [new file with mode: 0644]
tests/runner/MIException.cs [new file with mode: 0644]
tests/runner/MIResults.cs [new file with mode: 0644]
tests/runner/Runner.cs [new file with mode: 0644]
tests/runner/runner.csproj [new file with mode: 0644]
tests/simple_stepping/Program.cs [new file with mode: 0644]
tests/simple_stepping/simple_stepping.csproj [new file with mode: 0644]
tests/values/Program.cs [new file with mode: 0644]
tests/values/values.csproj [new file with mode: 0644]

diff --git a/netcoredbg.sln b/netcoredbg.sln
new file mode 100644 (file)
index 0000000..e2c4e25
--- /dev/null
@@ -0,0 +1,39 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.26124.0
+MinimumVisualStudioVersion = 15.0.26124.0
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{339D37EB-9697-4F56-926B-6083B37DD5D4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "runner", "tests\runner\runner.csproj", "{B6F87D26-7803-4944-BB93-0AFFE1AE6376}"
+EndProject
+Global
+       GlobalSection(SolutionConfigurationPlatforms) = preSolution
+               Debug|Any CPU = Debug|Any CPU
+               Debug|x64 = Debug|x64
+               Debug|x86 = Debug|x86
+               Release|Any CPU = Release|Any CPU
+               Release|x64 = Release|x64
+               Release|x86 = Release|x86
+       EndGlobalSection
+       GlobalSection(SolutionProperties) = preSolution
+               HideSolutionNode = FALSE
+       EndGlobalSection
+       GlobalSection(ProjectConfigurationPlatforms) = postSolution
+               {B6F87D26-7803-4944-BB93-0AFFE1AE6376}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+               {B6F87D26-7803-4944-BB93-0AFFE1AE6376}.Debug|Any CPU.Build.0 = Debug|Any CPU
+               {B6F87D26-7803-4944-BB93-0AFFE1AE6376}.Debug|x64.ActiveCfg = Debug|x64
+               {B6F87D26-7803-4944-BB93-0AFFE1AE6376}.Debug|x64.Build.0 = Debug|x64
+               {B6F87D26-7803-4944-BB93-0AFFE1AE6376}.Debug|x86.ActiveCfg = Debug|x86
+               {B6F87D26-7803-4944-BB93-0AFFE1AE6376}.Debug|x86.Build.0 = Debug|x86
+               {B6F87D26-7803-4944-BB93-0AFFE1AE6376}.Release|Any CPU.ActiveCfg = Release|Any CPU
+               {B6F87D26-7803-4944-BB93-0AFFE1AE6376}.Release|Any CPU.Build.0 = Release|Any CPU
+               {B6F87D26-7803-4944-BB93-0AFFE1AE6376}.Release|x64.ActiveCfg = Release|x64
+               {B6F87D26-7803-4944-BB93-0AFFE1AE6376}.Release|x64.Build.0 = Release|x64
+               {B6F87D26-7803-4944-BB93-0AFFE1AE6376}.Release|x86.ActiveCfg = Release|x86
+               {B6F87D26-7803-4944-BB93-0AFFE1AE6376}.Release|x86.Build.0 = Release|x86
+       EndGlobalSection
+       GlobalSection(NestedProjects) = preSolution
+               {B6F87D26-7803-4944-BB93-0AFFE1AE6376} = {339D37EB-9697-4F56-926B-6083B37DD5D4}
+       EndGlobalSection
+EndGlobal
diff --git a/tests/README.md b/tests/README.md
new file mode 100644 (file)
index 0000000..6367c62
--- /dev/null
@@ -0,0 +1,75 @@
+# netcoregbd tests
+
+## Running tests
+
+```
+git clone https://github.sec.samsung.net/i-kulaychuk/netcoredbg.git
+cd netcoredbg
+dotnet test
+```
+
+1. To run tests with dotnet:
+
+```
+CORERUN="/path/to/corerun" DEBUGGER="/path/to/debugger" dotnet test
+```
+
+2. To run tests with corerun:
+After ```dotnet build``` nuget packages will be downloaded into ~/.nuget
+To run tests with corerun, probably, you should copy some libraries from nuget packages into folder with corerun, like Microsoft.CodeAnalysis.*.dll and xunit.assert.dll (corerun will say if it miss some library).
+
+```
+dotnet build
+CORERUN="/path/to/corerun" DEBUGGER="/path/to/debugger" /path/to/corerun /path/to/launcher.dll
+```
+
+Result logs will appear in netcoredbg/tests/runner/bin/Debug/netcoreapp2.0/<test_start_date>
+
+## Creating test steps
+
+- Сreate some folder for test case inside 'tests' dir.
+- Сreate 'test_case_name.csproj' file and run ```dotnet sln add /path/to/test_case_name.csproj```.
+- Сreate 'test_case_name.cs' file with test case as written below.
+
+## Writing test case
+
+1. See example test case inside 'tests/example' folder.
+1. Write 'test_case_name.cs' file, which will be launched under the debugger.
+1. Write scenario for debugger in comments inside 'test_case_name.cs' file.
+
+### Writing scenario
+
+You should write scenario for debugger inside comments in 'test_case_name.cs' file, this comments will be executed with Roslyn.
+
+#### Multiline comments
+
+Don't use ```/**/``` for multiline comment. If you want to write comment on line with Roslyn commands - you should write it, using '/**/'.
+For multiline comments ```_currentLine``` and ```_commandLine``` variables will always be equal to number of first line.
+
+#### You must do
+
+- Write ```// $Main$``` tag on line with curly bracket after 'Main()'. At the beginning of every test case debugger running to Main.
+- Write ```// start()``` on line, where you want to start test case. Debugger will run to line with this comment.
+- Write ```// send("-gdb-exit")``` to finish test case.
+
+#### Global test case funciotns
+
+- ```MICore.ResultValue send(string cmd, int expectedLine = -1, int times = 1)``` - to send command 'cmd' to debugger. If command is '-exec-run', '-exec-continue', '-exec-step' or '-exec-next' - function will wait for the debugger to stop on 'expectedLine'. This function returns parsed respond: "\*stopped.*" for move commands and "\^done.*" for rest commands.
+- ```Match expect(string s)``` - to find line 's' in debugger output, using regexp and return 'RegularExpressions.Match'. If this function wont find match in 'EXPECT_TIMEOUT' seconds - it will throw exception.
+- ```void assertEqualRe(string expect, string actual, string msg = "", bool expectedRes = true)``` - to compare 'expect' and 'actual' strings with regexp and fail test, in case the result not equal to 'expectedRes'
+
+#### Global test case variables
+
+- ```int _current_line``` - has value of current line of '.cs' file. Functions 'nextTo()', 'stepTo()' and 'breakTo()' changing value of this variable, so if you use several of this functions inside single comment - '_current_line' variable will change while the comment is being executed.
+- ```int _command_line``` - has value of current line of '.cs' file. Unlike '_current_line', '_command_line' will have same value during all comment.
+- ```Dictionary<string, int> Tags``` - Dictionary, which contains line numbers with tags.
+  - write ```// $ tag_name $``` to add line number to 'Tags' dictionary with 'tag_name' key.
+
+#### Example
+
+```
+int x = 13;            // send("-exec-step", expectedLine: Tags["MY_TAG1"]);
+Console.WriteLine(x);  // /* $ MY_TAG1 $ */
+                       // var r = send(@"-var-create - * ""x""");
+                       // assertEqualRe("13", r.Find("value").ToString());
+```
diff --git a/tests/runner/MIException.cs b/tests/runner/MIException.cs
new file mode 100644 (file)
index 0000000..75e97b5
--- /dev/null
@@ -0,0 +1,137 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Globalization;
+
+namespace MICore
+{
+    public class MIException : Exception
+    {
+        public MIException(int hr)
+        {
+            this.HResult = hr;
+        }
+
+        public MIException(int hr, Exception innerException)
+            : base(string.Empty, innerException)
+        {
+        }
+    }
+
+    public class MIResultFormatException : MIException
+    {
+        private const int COMQC_E_BAD_MESSAGE = unchecked((int)0x80110604);
+        public readonly string Field;
+        public ResultValue Result;
+
+        public MIResultFormatException(string field, ResultValue value)
+            : base(COMQC_E_BAD_MESSAGE)
+        {
+            Field = field;
+            Result = value;
+        }
+
+        public MIResultFormatException(string field, ResultValue value, Exception innerException)
+            : base(COMQC_E_BAD_MESSAGE, innerException)
+        {
+            Field = field;
+            Result = value;
+        }
+
+        public override string Message
+        {
+            get
+            {
+                string message = string.Format(CultureInfo.CurrentCulture, "Unrecognized format of field \"{0}\" in result: {1}", Field, Result.ToString());
+                return message;
+            }
+        }
+    }
+
+    public class UnexpectedMIResultException : MIException
+    {
+        // We want to have a message which is vaguely reasonable if it winds up getting converted to an HRESULT. So
+        // we will use take this one.
+        //    MessageId: COMQC_E_BAD_MESSAGE
+        //
+        //    MessageText:
+        //      The message is improperly formatted or was damaged in transit
+        private const int COMQC_E_BAD_MESSAGE = unchecked((int)0x80110604);
+        private readonly string _debuggerName;
+        private readonly string _command;
+        private readonly string _miError;
+
+        /// <summary>
+        /// Creates a new UnexpectedMIResultException
+        /// </summary>
+        /// <param name="debuggerName">[Required] Name of the underlying MI debugger (ex: 'GDB')</param>
+        /// <param name="command">[Required] MI command that was issued</param>
+        /// <param name="mi">[Optional] Error message from MI</param>
+        public UnexpectedMIResultException(string debuggerName, string command, string mi) : base(COMQC_E_BAD_MESSAGE)
+        {
+            _debuggerName = debuggerName;
+            _command = command;
+            _miError = mi;
+        }
+
+        public override string Message
+        {
+            get
+            {
+                string message = string.Format(CultureInfo.CurrentCulture, "Unexpected {0} output from command \"{1}\".", _debuggerName, _command);
+                if (!string.IsNullOrWhiteSpace(_miError))
+                {
+                    message = string.Concat(message, " ", _miError);
+                }
+
+                return message;
+            }
+        }
+
+        public string MIError
+        {
+            get { return _miError; }
+        }
+    }
+
+    public class MIDebuggerInitializeFailedException : Exception
+    {
+        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes")]
+        public readonly IReadOnlyList<string> OutputLines;
+        private readonly string _debuggerName;
+        private readonly IReadOnlyList<string> _errorLines;
+        private string _message;
+
+        public MIDebuggerInitializeFailedException(string debuggerName, IReadOnlyList<string> errorLines, IReadOnlyList<string> outputLines)
+        {
+            this.OutputLines = outputLines;
+            _debuggerName = debuggerName;
+            _errorLines = errorLines;
+        }
+
+        public override string Message
+        {
+            get
+            {
+                if (_message == null)
+                {
+                    if (_errorLines.Any(x => !string.IsNullOrWhiteSpace(x)))
+                    {
+                        _message = string.Format(CultureInfo.InvariantCulture, "Unable to establish a connection to {0}. The following message was written to stderr:\n\n{1}\n\nSee Output Window for details.", _debuggerName, string.Join("\r\n", _errorLines));
+                    }
+                    else
+                    {
+                        _message = string.Format(CultureInfo.InvariantCulture, "Unable to establish a connection to {0}. Debug output may contain more information.", _debuggerName);
+                    }
+                }
+
+                return _message;
+            }
+        }
+    }
+}
diff --git a/tests/runner/MIResults.cs b/tests/runner/MIResults.cs
new file mode 100644 (file)
index 0000000..4c6c873
--- /dev/null
@@ -0,0 +1,1213 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Diagnostics;
+using System.Collections;
+using System.Globalization;
+
+namespace MICore
+{
+    /// <summary>
+    /// Prefix from the output indicating the class of result. According to the documentation, these are:
+    /// 
+    ///    result-class ==> "done" | "running" | "connected" | "error" | "exit" 
+    /// </summary>
+    public enum ResultClass
+    {
+        /// <summary>
+        /// ResultClass is not set
+        /// </summary>
+        None,
+
+        done,
+        running,
+        connected,
+        error,
+        exit
+    }
+
+    public class ResultValue
+    {
+        public virtual ResultValue Find(string name)
+        {
+            throw new MIResultFormatException(name, this);
+        }
+
+        public virtual bool TryFind(string name, out ResultValue result)
+        {
+            if (Contains(name))
+            {
+                result = Find(name);
+            }
+            else
+            {
+                result = null;
+            }
+            return result != null;
+        }
+
+        public virtual bool Contains(string name)
+        {
+            return false;
+        }
+
+        public ConstValue FindConst(string name)
+        {
+            return Find<ConstValue>(name);
+        }
+
+        public int FindInt(string name)
+        {
+            try
+            {
+                return FindConst(name).ToInt;
+            }
+            catch (MIResultFormatException)
+            {
+                throw;
+            }
+            catch (Exception e)
+            {
+                throw new MIResultFormatException(name, this, e);
+            }
+        }
+        public uint FindUint(string name)
+        {
+            try
+            {
+                return FindConst(name).ToUint;
+            }
+            catch (MIResultFormatException)
+            {
+                throw;
+            }
+            catch (Exception e)
+            {
+                throw new MIResultFormatException(name, this, e);
+            }
+        }
+
+        /// <summary>
+        /// Try to find a uint property. Throws if the property can be found but is not a uint.
+        /// </summary>
+        /// <param name="name">[Required] name of the property to search for</param>
+        /// <returns>The value of the property or null if it cannot be found</returns>
+        public uint? TryFindUint(string name)
+        {
+            ConstValue c;
+            if (!TryFind(name, out c))
+            {
+                return null;
+            }
+
+            try
+            {
+                return c.ToUint;
+            }
+            catch (OverflowException)
+            {
+                return null;
+            }
+            catch (MIResultFormatException)
+            {
+                throw;
+            }
+            catch (Exception e)
+            {
+                throw new MIResultFormatException(name, this, e);
+            }
+        }
+
+        public ulong FindAddr(string name)
+        {
+            try
+            {
+                return FindConst(name).ToAddr;
+            }
+            catch (MIResultFormatException)
+            {
+                throw;
+            }
+            catch (Exception e)
+            {
+                throw new MIResultFormatException(name, this, e);
+            }
+        }
+
+        /// <summary>
+        /// Try and find an address property. Returns null if there is no property. Will throw if that property exists but it is not an address.
+        /// </summary>
+        /// <param name="name">[Required] Name of the property to look for</param>
+        /// <returns>The value of the address or null if it can't be found</returns>
+        public ulong? TryFindAddr(string name)
+        {
+            ConstValue c;
+            if (!TryFind(name, out c))
+            {
+                return null;
+            }
+
+            try
+            {
+                return c.ToAddr;
+            }
+            catch (MIResultFormatException)
+            {
+                throw;
+            }
+            catch (Exception e)
+            {
+                throw new MIResultFormatException(name, this, e);
+            }
+        }
+
+
+        public string FindString(string name)
+        {
+            return FindConst(name).AsString;
+        }
+
+        public string TryFindString(string name)
+        {
+            ConstValue c;
+            if (!TryFind(name, out c))
+            {
+                return string.Empty;
+            }
+            return c.AsString;
+        }
+
+        public T Find<T>(string name) where T : ResultValue
+        {
+            var c = Find(name);
+            if (c is T)
+            {
+                return c as T;
+            }
+            throw new MIResultFormatException(name, this);
+        }
+
+        public bool TryFind<T>(string name, out T result) where T : ResultValue
+        {
+            if (Contains(name))
+            {
+                result = Find(name) as T;
+            }
+            else
+            {
+                result = null;
+            }
+            return result != null;
+        }
+
+        public T TryFind<T>(string name) where T : ResultValue
+        {
+            T result;
+            if (!TryFind(name, out result))
+            {
+                return null;
+            }
+            return result;
+        }
+    }
+
+    public class ConstValue : ResultValue
+    {
+        public readonly string Content;
+
+        public ConstValue(string str)
+        {
+            Content = str ?? string.Empty;
+        }
+
+        public static ulong ParseAddr(string addr, bool throwOnError = false)
+        {
+            ulong res = 0;
+            if (string.IsNullOrEmpty(addr))
+            {
+                if (throwOnError)
+                {
+                    throw new ArgumentNullException();
+                }
+                return 0;
+            }
+            else if (addr.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
+            {
+                if (throwOnError)
+                {
+                    res = ulong.Parse(addr.Substring(2), System.Globalization.NumberStyles.HexNumber, CultureInfo.InvariantCulture);
+                }
+                else
+                {
+                    ulong.TryParse(addr.Substring(2), System.Globalization.NumberStyles.HexNumber, CultureInfo.InvariantCulture, out res);
+                }
+            }
+            else
+            {
+                if (throwOnError)
+                {
+                    res = ulong.Parse(addr, System.Globalization.NumberStyles.Integer, CultureInfo.InvariantCulture);
+                }
+                else
+                {
+                    ulong.TryParse(addr, System.Globalization.NumberStyles.Integer, CultureInfo.InvariantCulture, out res);
+                }
+            }
+            return res;
+        }
+
+        public static uint ParseUint(string str, bool throwOnError = false)
+        {
+            uint value = 0;
+            if (string.IsNullOrEmpty(str))
+            {
+                if (throwOnError)
+                {
+                    throw new ArgumentException();
+                }
+                return value;
+            }
+            else if (str.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
+            {
+                if (throwOnError)
+                {
+                    value = uint.Parse(str.Substring(2), System.Globalization.NumberStyles.HexNumber, CultureInfo.InvariantCulture);
+                }
+                else
+                {
+                    uint.TryParse(str.Substring(2), System.Globalization.NumberStyles.HexNumber, CultureInfo.InvariantCulture, out value);
+                }
+            }
+            else
+            {
+                if (throwOnError)
+                {
+                    value = uint.Parse(str, System.Globalization.NumberStyles.Integer, CultureInfo.InvariantCulture);
+                }
+                else
+                {
+                    uint.TryParse(str, System.Globalization.NumberStyles.Integer, CultureInfo.InvariantCulture, out value);
+                }
+            }
+            return value;
+        }
+
+        public ulong ToAddr
+        {
+            get
+            {
+                return ParseAddr(Content, throwOnError: true);
+            }
+        }
+        public int ToInt
+        {
+            get
+            {
+                return int.Parse(Content, CultureInfo.InvariantCulture);
+            }
+        }
+        public uint ToUint
+        {
+            get
+            {
+                return ParseUint(Content, throwOnError: true);
+            }
+        }
+
+        public string AsString
+        {
+            get
+            {
+                return Content;
+            }
+        }
+
+        public override string ToString()
+        {
+            return Content;
+        }
+    }
+
+    [DebuggerDisplay("{DisplayValue,nq}", Name = "{Name,nq}")]
+    [DebuggerTypeProxy(typeof(NamedResultValueTypeProxy))]
+    public class NamedResultValue
+    {
+        internal class NamedResultValueTypeProxy
+        {
+            private ResultValue _value;
+            public NamedResultValueTypeProxy(NamedResultValue namedResultValue)
+            {
+                _value = namedResultValue.Value;
+            }
+
+            [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+            public NamedResultValue[] Content
+            {
+                get
+                {
+                    List<NamedResultValue> values = null;
+
+                    if (_value is ValueListValue)
+                    {
+                        var valueListValue = (ValueListValue)_value;
+                        values = new List<NamedResultValue>(valueListValue.Length);
+                        for (int i = 0; i < valueListValue.Length; i++)
+                        {
+                            string name = string.Format(CultureInfo.InvariantCulture, "[{0}]", i); // fake the [0], [1], [2], etc...
+                            values.Add(new NamedResultValue(name, valueListValue.Content[i]));
+                        }
+                    }
+                    else if (_value is ResultListValue)
+                    {
+                        var resultListValue = (ResultListValue)_value;
+                        var namedResultValues = resultListValue.Content.Select(value => new NamedResultValue(value));
+                        values = new List<NamedResultValue>(namedResultValues);
+                    }
+                    else if (_value is TupleValue)
+                    {
+                        var tupleValue = (TupleValue)_value;
+                        values = new List<NamedResultValue>(tupleValue.Content.Count);
+                        tupleValue.Content.ForEach((namedResultValue) =>
+                        {
+                            values.Add(new NamedResultValue(namedResultValue.Name, namedResultValue.Value));
+                        });
+                    }
+
+                    return values?.ToArray();
+                }
+            }
+        }
+
+        public string Name { get; private set; }
+        public ResultValue Value { get; private set; }
+
+        public NamedResultValue(string name, ResultValue value)
+        {
+            this.Name = name;
+            this.Value = value;
+        }
+
+        public NamedResultValue(NamedResultValue namedResultValue) : this(namedResultValue.Name, namedResultValue.Value)
+        {
+        }
+
+        internal string DisplayValue
+        {
+            get
+            {
+                if (this.Value is ConstValue)
+                {
+                    return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", ((ConstValue)this.Value).AsString);
+                }
+                else if (this.Value is TupleValue)
+                {
+                    return string.Format(CultureInfo.InvariantCulture, "{{...}}");
+                }
+                else if (this.Value is ListValue)
+                {
+                    return string.Format(CultureInfo.InvariantCulture, "[...] count = {0}", ((ListValue)this.Value).Length);
+                }
+
+                return "<Unkonwn ResultValue Type>";
+            }
+        }
+
+        public override string ToString()
+        {
+            StringBuilder builder = new StringBuilder();
+            builder.Append(Name);
+            builder.Append("=");
+            builder.Append(Value.ToString());
+            return builder.ToString();
+        }
+    }
+
+    public class TupleValue : ResultValue
+    {
+        public List<NamedResultValue> Content { get; private set; }
+
+        public TupleValue(List<NamedResultValue> list)
+        {
+            Content = list;
+        }
+        public override ResultValue Find(string name)
+        {
+            var item = Content.Find((c) => c.Name == name);
+            if (item == null)
+            {
+                throw new MIResultFormatException(name, this);
+            }
+            return item.Value;
+        }
+
+        public override bool Contains(string name)
+        {
+            var item = Content.Find((c) => c.Name == name);
+            return item != null;
+        }
+
+        public override string ToString()
+        {
+            StringBuilder outTuple = new StringBuilder();
+            outTuple.Append("{");
+            for (int i = 0; i < Content.Count; ++i)
+            {
+                if (i != 0)
+                {
+                    outTuple.Append(",");
+                }
+                outTuple.Append(Content[i].ToString());
+            }
+            outTuple.Append("}");
+            return outTuple.ToString();
+        }
+        public ResultValue[] FindAll(string name)
+        {
+            return Content.FindAll((c) => c.Name == name).Select((c) => c.Value).ToArray();
+        }
+
+        public T[] FindAll<T>(string name) where T : class
+        {
+            return FindAll(name).OfType<T>().ToArray();
+        }
+
+        /// <summary>
+        /// Creates a new TupleValue with a subset of values from this TupleValue.
+        /// </summary>
+        /// <param name="requiredNames">The list of names that must be added to the TupleValue.</param>
+        /// <param name="optionalNames">The list of names that will be added to the TupleValue if they exist in this TupleValue.</param>
+        public TupleValue Subset(IEnumerable<string> requiredNames, IEnumerable<string> optionalNames = null)
+        {
+            List<NamedResultValue> values = new List<NamedResultValue>();
+
+            // Iterate the required list and add the values.
+            // Will throw if a name cannot be found.
+            foreach (string name in requiredNames)
+            {
+                values.Add(new NamedResultValue(name, this.Find(name)));
+            }
+
+            // Iterate the optional list and add the values of the name exists.
+            if (null != optionalNames)
+            {
+                foreach (string name in optionalNames)
+                {
+                    ResultValue value;
+                    if (this.TryFind(name, out value))
+                    {
+                        values.Add(new NamedResultValue(name, value));
+                    }
+                }
+            }
+
+            return new TupleValue(values);
+        }
+    }
+
+    public abstract class ListValue : ResultValue
+    {
+        public abstract int Length { get; }
+        public bool IsEmpty()
+        {
+            return this.Length == 0;
+        }
+    }
+
+    public class ValueListValue : ListValue
+    {
+        public ResultValue[] Content { get; private set; }
+
+        public override int Length { get { return Content.Length; } }
+
+        public ValueListValue(List<ResultValue> list)
+        {
+            Content = list.ToArray();
+        }
+        public T[] AsArray<T>() where T : ResultValue
+        {
+            return Content.Cast<T>().ToArray();
+        }
+
+        public string[] AsStrings
+        {
+            get { return Content.Cast<ConstValue>().Select(c => c.AsString).ToArray(); }
+        }
+
+        public override string ToString()
+        {
+            StringBuilder outList = new StringBuilder();
+            outList.Append("[");
+            for (int i = 0; i < Content.Length; ++i)
+            {
+                if (i != 0)
+                {
+                    outList.Append(",");
+                }
+                outList.Append(Content[i].ToString());
+            }
+            outList.Append("]");
+            return outList.ToString();
+        }
+    }
+
+    public class ResultListValue : ListValue
+    {
+        public NamedResultValue[] Content { get; private set; }
+
+        public override int Length { get { return Content.Length; } }
+
+        public ResultListValue(List<NamedResultValue> list)
+        {
+            Content = list.ToArray();
+        }
+        public override ResultValue Find(string name)
+        {
+            var item = Array.Find(Content, (c) => c.Name == name);
+            if (item == null)
+            {
+                throw new MIResultFormatException(name, this);
+            }
+            return item.Value;
+        }
+
+        public override bool Contains(string name)
+        {
+            var item = Array.Find(Content, (c) => c.Name == name);
+            return item != null;
+        }
+
+        public ResultValue[] FindAll(string name)
+        {
+            return Array.FindAll(Content, (c) => c.Name == name).Select((c) => c.Value).ToArray();
+        }
+
+        public T[] FindAll<T>(string name) where T : class
+        {
+            return FindAll(name).OfType<T>().ToArray();
+        }
+
+        public string[] FindAllStrings(string name)
+        {
+            return FindAll<ConstValue>(name).Select((c) => c.AsString).ToArray();
+        }
+
+        public int CountOf(string name)
+        {
+            return Content.Count(c => c.Name == name);
+        }
+
+        public override string ToString()
+        {
+            StringBuilder outList = new StringBuilder();
+            outList.Append("[");
+            for (int i = 0; i < Content.Length; ++i)
+            {
+                if (i != 0)
+                {
+                    outList.Append(",");
+                }
+                outList.Append(Content[i].Name);
+                outList.Append("=");
+                outList.Append(Content[i].Value.ToString());
+            }
+            outList.Append("]");
+            return outList.ToString();
+        }
+    }
+
+    [DebuggerTypeProxy(typeof(ResultsTypeProxy))]
+    [DebuggerDisplay("{ResultClass}, Length={Length}")]
+    public class Results : ResultListValue
+    {
+        internal class ResultsTypeProxy
+        {
+            public ResultsTypeProxy(Results results)
+            {
+                this.Content = results.Content;
+            }
+
+            [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+            public NamedResultValue[] Content { get; private set; }
+        }
+
+        public readonly ResultClass ResultClass;
+
+        public Results(ResultClass resultsClass, List<NamedResultValue> list = null)
+            : base(list ?? new List<NamedResultValue>())
+        {
+            ResultClass = resultsClass;
+        }
+
+        public Results Add(string name, ResultValue value)
+        {
+            var l = Content.ToList();
+            l.Add(new NamedResultValue(name, value));
+            return new Results(ResultClass, l);
+        }
+
+        public override string ToString()
+        {
+            StringBuilder outList = new StringBuilder();
+            outList.Append("result-class: " + ResultClass.ToString());
+            for (int i = 0; i < Content.Length; ++i)
+            {
+                outList.Append("\r\n");
+                outList.Append(Content[i].Name);
+                outList.Append(": ");
+                outList.Append(Content[i].Value.ToString());
+            }
+            outList.Append("\r\n");
+            return outList.ToString();
+        }
+    };
+
+    public class MIResults
+    {
+        struct Span
+        {
+            static Span _emptySpan;
+            public int Start { get; private set; }  // index first character in the substring
+            public int Length { get; private set; } // length of the substring
+            public int Extent { get { return Start + Length; } }
+            public bool IsEmpty { get { return Length == 0; } }
+            public static Span Empty { get { return _emptySpan; } }
+
+            static Span()
+            {
+                _emptySpan = new Span(0, 0);
+            }
+
+            public Span(string s)
+            {
+                Start = 0;
+                Length = s.Length;
+            }
+            public Span(int start, int len)
+            {
+                if (start < 0)
+                {
+                    throw new ArgumentException("start");
+                }
+                Start = start;
+                Length = len;
+            }
+            public Span Advance(int len)
+            {
+                if (len > Length)
+                {
+                    throw new ArgumentException("len");
+                }
+                return new Span(Start + len, Length - len);
+            }
+            public Span AdvanceTo(int pos)
+            {
+                if (Start > pos || pos > Start + Length)
+                {
+                    throw new ArgumentException("pos");
+                }
+                return new Span(pos, Length - (pos - Start));
+            }
+            public Span Prefix(int len)
+            {
+                if (len > Length)
+                {
+                    throw new ArgumentException("len");
+                }
+                return new Span(Start, len);
+            }
+            public string Extract(string theString)
+            {
+                if (Extent > theString.Length)
+                {
+                    throw new ArgumentException("theSpan");
+                }
+                return theString.Substring(Start, Length);
+            }
+            public int IndexOf(string theString, char c)
+            {
+                int i = theString.IndexOf(c, Start);
+                if (i < 0 || i >= Extent)
+                {
+                    return -1;
+                }
+                return i - Start;   // Span relative offset
+            }
+            public bool StartsWith(string theString, string pattern)
+            {
+                if (Length < pattern.Length)
+                {
+                    return false;
+                }
+                for (int i = 0; i < pattern.Length; ++i)
+                {
+                    if (theString[Start + i] != pattern[i])
+                    {
+                        return false;
+                    }
+                }
+                return true;
+            }
+        }
+
+        private string _resultString;
+
+        /// <summary>
+        /// result-record ==> result-class ( "," result )* 
+        /// </summary>
+        /// <param name="output"></param>
+        public Results ParseCommandOutput(string output)
+        {
+            _resultString = output.Trim();
+            int comma = _resultString.IndexOf(',');
+            Results results;
+            ResultClass resultClass = ResultClass.None;
+            if (comma < 0)
+            {
+                // no comma, so entire string should be the result class
+                results = new Results(ParseResultClass(output), new List<NamedResultValue>());
+            }
+            else
+            {
+                resultClass = ParseResultClass(output.Substring(0, comma));
+                Span wholeString = new Span(_resultString);
+                results = ParseResultList(wholeString.AdvanceTo(comma + 1), resultClass);
+            }
+            return results;
+        }
+
+        public Results ParseResultList(string listStr, ResultClass resultClass = ResultClass.None)
+        {
+            _resultString = listStr.Trim();
+            return ParseResultList(new Span(_resultString), resultClass);
+        }
+
+        private Results ParseResultList(Span listStr, ResultClass resultClass = ResultClass.None)
+        {
+            Span rest;
+            var list = ParseResultList((Span s, ref int i) =>
+                {
+                    return true;
+                }, (Span s, ref int i) =>
+                    {
+                        return i == s.Extent;
+                    }, listStr, out rest);
+            if (!rest.IsEmpty)
+            {
+                ParseError("trailing chars", rest);
+                return null;
+            }
+            return new Results(resultClass, list);
+        }
+
+        public string ParseCString(string input)
+        {
+            if (input == null)
+            {
+                throw new ArgumentNullException("input");
+            }
+            else if (input == String.Empty)
+            {
+                return string.Empty;
+            }
+
+            string cstr = input.Trim();
+            if (cstr[0] != '\"')   // not a Cstring, just return the string
+            {
+                return input;
+            }
+            _resultString = cstr;
+            Span rest;
+            var s = ParseCString(new Span(cstr), out rest);
+            return s == null ? string.Empty : s.AsString;
+        }
+
+        private string ParseCString(Span input)
+        {
+            if (input.IsEmpty)
+            {
+                return string.Empty;
+            }
+
+            if (_resultString[input.Start] != '\"')   // not a Cstring, just return the string
+            {
+                return input.Extract(_resultString);
+            }
+            Span rest;
+            var s = ParseCString(input, out rest);
+            return s == null ? string.Empty : s.AsString;
+        }
+
+        /// <summary>
+        /// value ==>const | tuple | list
+        /// </summary>
+        /// <returns></returns>
+        private ResultValue ParseValue(Span resultStr, out Span rest)
+        {
+            ResultValue value = null;
+            rest = Span.Empty;
+            if (resultStr.IsEmpty)
+            {
+                return null;
+            }
+            switch (_resultString[resultStr.Start])
+            {
+                case '\"':
+                    value = ParseCString(resultStr, out rest);
+                    break;
+                case '{':
+                    value = ParseTuple(resultStr, out rest);
+                    break;
+                case '[':
+                    value = ParseList(resultStr, out rest);
+                    break;
+                default:
+                    ParseError("unexpected char", resultStr);
+                    break;
+            }
+            return value;
+        }
+
+        /// <summary>
+        /// GDB (on x86) sometimes returns a tuple list in a context requiring a tuple (&lt;MULTIPLE&gt; breakpoints). 
+        /// The grammer does not allow this, but we recognize it and accept it only in the special case when it is contained
+        /// in a result value.
+        ///     tuplelist --  tuple ("," tuple)*
+        ///     value -- const | tuple | tuplelist | list
+        /// </summary>
+        /// <returns></returns>
+        private ResultValue ParseResultValue(Span resultStr, out Span rest)
+        {
+            ResultValue value = null;
+            rest = Span.Empty;
+            if (resultStr.IsEmpty)
+            {
+                return null;
+            }
+            switch (_resultString[resultStr.Start])
+            {
+                case '\"':
+                    value = ParseCString(resultStr, out rest);
+                    break;
+                case '{':
+                    value = ParseResultTuple(resultStr, out rest);
+                    break;
+                case '[':
+                    value = ParseList(resultStr, out rest);
+                    break;
+                default:
+                    ParseError("unexpected char", resultStr);
+                    break;
+            }
+            return value;
+        }
+
+        /// <summary>
+        /// IsValueChar - true is the char is a start-char for a value
+        /// </summary>
+        private static bool IsValueChar(char c)
+        {
+            return c == '\"' || c == '{' || c == '[';
+        }
+
+        /// <summary>
+        /// result ==> variable "=" value
+        /// </summary>
+        /// <param name="resultStr">trimmed input string</param>
+        /// <param name="rest">trimmed remainder after result</param>
+        private NamedResultValue ParseResult(Span resultStr, out Span rest)
+        {
+            rest = Span.Empty;
+            int equals = resultStr.IndexOf(_resultString, '=');
+            if (equals < 1)
+            {
+                ParseError("variable not found", resultStr);
+                return null;
+            }
+            string name = resultStr.Prefix(equals).Extract(_resultString);
+            ResultValue value = ParseResultValue(resultStr.Advance(equals + 1), out rest);
+            if (value == null)
+            {
+                return null;
+            }
+            return new NamedResultValue(name, value);
+        }
+
+        private static ResultClass ParseResultClass(string resultClass)
+        {
+            switch (resultClass)
+            {
+                case "done": return ResultClass.done;
+                case "running": return ResultClass.running;
+                case "connected": return ResultClass.connected;
+                case "error": return ResultClass.error;
+                case "exit": return ResultClass.exit;
+                default:
+                    {
+                        Debug.Fail("unexpected result class");
+                        return ResultClass.None;
+                    }
+            }
+        }
+
+        private ConstValue ParseCString(Span input, out Span rest)
+        {
+            rest = input;
+            StringBuilder output = new StringBuilder();
+            if (input.IsEmpty || _resultString[input.Start] != '\"')
+            {
+                ParseError("Cstring expected", input);
+                return null;
+            }
+            int i = input.Start + 1;
+            bool endFound = false;
+            for (; i < input.Extent; i++)
+            {
+                char c = _resultString[i];
+                if (c == '\"')
+                {
+                    // closing quote, so we are (probably) done
+                    i++;
+                    if ((i < input.Extent) && (_resultString[i] == c))
+                    {
+                        // double quotes mean we emit a single quote, and carry on
+                        ;
+                    }
+                    else
+                    {
+                        endFound = true;
+                        break;
+                    }
+                }
+                else if (c == '\\')
+                {
+                    // escaped character
+                    c = _resultString[++i];
+                    switch (c)
+                    {
+                        case 'n': c = '\n'; break;
+                        case 'r': c = '\r'; break;
+                        case 't': c = '\t'; break;
+                        default: break;
+                    }
+                }
+                output.Append(c);
+            }
+            if (!endFound)
+            {
+                ParseError("CString not terminated", input);
+                return null;
+            }
+            rest = input.AdvanceTo(i);
+            return new ConstValue(output.ToString());
+        }
+
+        private delegate bool EdgeCondition(Span s, ref int i);
+
+        private List<NamedResultValue> ParseResultList(EdgeCondition begin, EdgeCondition end, Span input, out Span rest)
+        {
+            rest = Span.Empty;
+            List<NamedResultValue> list = new List<NamedResultValue>();
+            int i = input.Start;
+            if (!begin(input, ref i))
+            {
+                ParseError("Unexpected opening character", input);
+                return null;
+            }
+            if (end(input, ref i))    // tuple is empty
+            {
+                rest = input.AdvanceTo(i);  // eat through the closing brace
+                return list;
+            }
+            input = input.AdvanceTo(i);
+            var item = ParseResult(input, out rest);
+            if (item == null)
+            {
+                ParseError("Result expected", input);
+                return null;
+            }
+            list.Add(item);
+            input = rest;
+            while (!input.IsEmpty && _resultString[input.Start] == ',')
+            {
+                item = ParseResult(input.Advance(1), out rest);
+                if (item == null)
+                {
+                    ParseError("Result expected", input);
+                    return null;
+                }
+                list.Add(item);
+                input = rest;
+            }
+
+            i = input.Start;
+            if (!end(input, ref i))    // tuple is not closed
+            {
+                ParseError("Unexpected list termination", input);
+                rest = Span.Empty;
+                return null;
+            }
+            rest = input.AdvanceTo(i);
+            return list;
+        }
+
+        private List<NamedResultValue> ParseResultList(char begin, char end, Span input, out Span rest)
+        {
+            return ParseResultList((Span s, ref int i) =>
+            {
+                if (_resultString[i] == begin)
+                {
+                    i++;
+                    return true;
+                }
+                return false;
+            }, (Span s, ref int i) =>
+            {
+                if (i < s.Extent && _resultString[i] == end)
+                {
+                    i++;
+                    return true;
+                }
+                return false;
+            }, input, out rest);
+        }
+
+        /// <summary>
+        /// tuple ==> "{}" | "{" result ( "," result )* "}" 
+        /// </summary>
+        /// <returns>if one tuple found a TupleValue, otherwise a ValueListValue of TupleValues</returns>
+        private ResultValue ParseResultTuple(Span input, out Span rest)
+        {
+            var list = ParseResultList('{', '}', input, out rest);
+            if (list == null)
+            {
+                return null;
+            }
+            var tlist = new List<ResultValue>();
+            TupleValue v;
+            while (rest.StartsWith(_resultString, ",{"))
+            {
+                // a tuple list
+                v = new TupleValue(list);
+                tlist.Add(v);
+                list = ParseResultList('{', '}', rest.Advance(1), out rest);
+            }
+            v = new TupleValue(list);
+            if (tlist.Count != 0)
+            {
+                tlist.Add(v);
+                return new ValueListValue(tlist);
+            }
+            return v;
+        }
+
+        /// <summary>
+        /// tuple ==> "{}" | "{" result ( "," result )* "}" 
+        /// </summary>
+        private TupleValue ParseTuple(Span input, out Span rest)
+        {
+            var list = ParseResultList('{', '}', input, out rest);
+            if (list == null)
+            {
+                return null;
+            }
+            return new TupleValue(list);
+        }
+
+        /// <summary>
+        /// list ==> "[]" | "[" value ( "," value )* "]" | "[" result ( "," result )* "]"  
+        /// </summary>
+        private ResultValue ParseList(Span input, out Span rest)
+        {
+            rest = Span.Empty;
+            if (_resultString[input.Start] != '[')
+            {
+                ParseError("List expected", input);
+                return null;
+            }
+            if (_resultString[input.Start + 1] == ']')    // list is empty
+            {
+                rest = input.Advance(2);  // eat through the closing brace
+                return new ValueListValue(new List<ResultValue>());
+            }
+            if (IsValueChar(_resultString[input.Start + 1]))
+            {
+                return ParseValueList(input, out rest);
+            }
+            else
+            {
+                return ParseResultList(input, out rest);
+            }
+        }
+
+        /// <summary>
+        /// list ==>  "[" value ( "," value )* "]"   
+        /// </summary>
+        private ValueListValue ParseValueList(Span input, out Span rest)
+        {
+            rest = Span.Empty;
+            List<ResultValue> list = new List<ResultValue>();
+            if (_resultString[input.Start] != '[')
+            {
+                ParseError("List expected", input);
+                return null;
+            }
+            input = input.Advance(1);
+            var item = ParseValue(input, out rest);
+            if (item == null)
+            {
+                ParseError("Value expected", input);
+                return null;
+            }
+            list.Add(item);
+            input = rest;
+            while (!input.IsEmpty && _resultString[input.Start] == ',')
+            {
+                item = ParseValue(input.Advance(1), out rest);
+                if (item == null)
+                {
+                    ParseError("Value expected", input);
+                    return null;
+                }
+                list.Add(item);
+                input = rest;
+            }
+
+            if (input.IsEmpty || _resultString[input.Start] != ']')    // list is not closed
+            {
+                ParseError("List not terminated", input);
+                rest = Span.Empty;
+                return null;
+            }
+            rest = input.Advance(1);
+            return new ValueListValue(list);
+        }
+
+        /// <summary>
+        /// list ==>  "[" result ( "," result )* "]"  
+        /// </summary>
+        private ResultListValue ParseResultList(Span input, out Span rest)
+        {
+            var list = ParseResultList('[', ']', input, out rest);
+            if (list == null)
+            {
+                return null;
+            }
+            return new ResultListValue(list);
+        }
+
+        private void ParseError(string message, Span input)
+        {
+            if (input.Length > 1000)
+            {
+                input = new Span(input.Start, 1000);    // don't show more than 1000 chars
+            }
+            string result = input.Extract(_resultString);
+            Debug.Fail(message + ": " + result);
+        }
+    }
+}
diff --git a/tests/runner/Runner.cs b/tests/runner/Runner.cs
new file mode 100644 (file)
index 0000000..67f1d84
--- /dev/null
@@ -0,0 +1,362 @@
+using System;
+using System.Diagnostics;
+using System.Text;
+using System.IO;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using System.Threading.Tasks.Dataflow;
+using Xunit;
+using Xunit.Abstractions;
+
+using System.Text.RegularExpressions;
+using System.Linq;
+using System.Reflection;
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Scripting;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using System.Runtime.CompilerServices;
+using Microsoft.CodeAnalysis.Scripting;
+
+using System.Threading;
+
+namespace Runner
+{
+    public class Labeled<T>
+    {
+        public Labeled(T data, string label)
+        {
+            Data = data;
+            Label = label;
+        }
+
+        public T Data { get; }
+        public string Label { get; }
+
+        public override string ToString()
+        {
+            return Label;
+        }
+    }
+
+    public static class Labeledextensions
+    {
+        public static Labeled<T> Labeled<T>
+                (this T source, string label)=>new Labeled<T>( source, label );
+    }
+
+    public partial class TestRunner
+    {
+        public static IEnumerable<object[]> Data()
+        {
+            object[] make
+            (string binName,
+             string srcName,
+             string label)
+            {
+                return new object []
+                { ( binName:binName
+                , srcName:srcName
+                )
+                .Labeled(label)
+                };
+            }
+            var data = new List<object[]>();
+
+            // Sneaky way to get assembly path, which works even if call
+            // current constructor with reflection
+            string codeBase = Assembly.GetExecutingAssembly().CodeBase;
+            UriBuilder uri = new UriBuilder(codeBase);
+            string path = Uri.UnescapeDataString(uri.Path);
+            var d = new DirectoryInfo(Path.GetDirectoryName(path));
+
+            // Get path to runner binaries
+            path = Path.Combine(d.Parent.Parent.Parent.Parent.FullName, "runner");
+            var files = Directory.GetFiles(path, "*.dll", SearchOption.AllDirectories);
+            var depsJson = new FileInfo(files[0].Substring(0, files[0].Length - 4) + ".deps.json");
+            var runnerPath = depsJson.Directory.Parent.Parent.Parent.FullName;
+
+            // Find all dlls
+            var baseDir = d.Parent.Parent.Parent.Parent;
+            files = Directory.GetFiles(baseDir.FullName, "*.dll", SearchOption.AllDirectories);
+
+            foreach (var dll in files)
+            {
+                string testName = dll.Substring(0, dll.Length - 4);
+                depsJson = new FileInfo(testName + ".deps.json");
+                var dllDir = depsJson.Directory.Parent.Parent.Parent.FullName;
+                // Do not use as test cases runner and launcher files
+                if (depsJson.Exists &&
+                    !dllDir.Equals(runnerPath, StringComparison.CurrentCultureIgnoreCase))
+                {
+                    var csFiles = Directory.GetFiles(depsJson.Directory.Parent.Parent.Parent.FullName, "*.cs");
+                    data.Add(make(dll, csFiles[0], testName.Split('/').Last()));
+                }
+            }
+
+            return data;
+        }
+
+        public class ProcessInfo
+        {
+            public ProcessInfo(string binName, ITestOutputHelper output)
+            {
+                this.output = output;
+                process = new Process();
+                queue = new BufferBlock<string>();
+
+                process.StartInfo.CreateNoWindow = true;
+                process.StartInfo.RedirectStandardOutput = true;
+                process.StartInfo.RedirectStandardInput = true;
+                process.StartInfo.UseShellExecute = false;
+                process.StartInfo.Arguments = "";
+                process.StartInfo.FileName = binName;
+
+                // enable raising events because Process does not raise events by default
+                process.EnableRaisingEvents = true;
+                // attach the event handler for OutputDataReceived before starting the process
+                process.OutputDataReceived += new DataReceivedEventHandler
+                (
+                    delegate(object sender, DataReceivedEventArgs e)
+                    {
+                        if (e.Data is null)
+                            return;
+                        // append the new data to the data already read-in
+                        output.WriteLine("> " + e.Data);
+                        queue.Post(e.Data);
+                    }
+                );
+                process.Exited += new EventHandler
+                (
+                    delegate(object sender, EventArgs args)
+                    {
+                        // This is where you can add some code to be
+                        // executed before this program exits.
+                        queue.Complete();
+                    }
+                );
+
+                try
+                {
+                    process.Start();
+                }
+                catch (System.ComponentModel.Win32Exception)
+                {
+                    throw new Exception("Unable to run process: " + binName);
+                }
+
+                process.BeginOutputReadLine();
+            }
+
+            public void Close()
+            {
+                process.StandardInput.Close();
+                if (!process.WaitForExit(5))
+                {
+                    process.CancelOutputRead();
+                    process.Close();
+                }
+                else
+                    process.CancelOutputRead();
+            }
+
+            public string Receive()
+            {
+                return queue.ReceiveAsync().Result;
+            }
+
+            public MICore.Results Expect(string text, int timeoutSec = 10)
+            {
+                TimeSpan timeSpan = TimeSpan.FromSeconds(timeoutSec);
+
+                CancellationTokenSource ts = new CancellationTokenSource();
+
+                ts.CancelAfter(timeSpan);
+                CancellationToken token = ts.Token;
+                token.ThrowIfCancellationRequested();
+
+                try
+                {
+                    while (true)
+                    {
+                        Task<string> intputTask = queue.ReceiveAsync();
+                        intputTask.Wait(token);
+                        string result = intputTask.Result;
+                        if (result.StartsWith(text))
+                        {
+                            var parser = new MICore.MIResults();
+                            return parser.ParseCommandOutput(result);
+                        }
+                    }
+                }
+                catch (AggregateException e)
+                {
+                    foreach (var v in e.InnerExceptions)
+                        output.WriteLine(e.Message + " " + v.Message);
+                }
+                catch (OperationCanceledException)
+                {
+                }
+                finally
+                {
+                    ts.Dispose();
+                }
+                throw new Exception($"Expected '{text}' in {timeSpan}");
+            }
+
+            public void Send(string s)
+            {
+                process.StandardInput.WriteLine(s);
+                output.WriteLine("< " + s);
+            }
+
+            public Process process;
+            private readonly ITestOutputHelper output;
+            public BufferBlock<string> queue;
+        }
+
+        public class TestCaseGlobals
+        {
+            public readonly Dictionary<string, int> Lines;
+            private ProcessInfo processInfo;
+            public TestCaseGlobals(
+                ProcessInfo processInfo,
+                Dictionary<string, int> lines,
+                string testSource,
+                string testBin,
+                ITestOutputHelper output)
+            {
+                this.processInfo = processInfo;
+                this.Lines = lines;
+                this.TestSource = testSource;
+                this.TestBin = testBin;
+                this.Output = output;
+            }
+            public int GetCurrentLine([CallerLineNumber] int line = 0) { return line; }
+
+            public void Send(string s) => processInfo.Send(s);
+            public MICore.Results Expect(string s) => processInfo.Expect(s);
+            public readonly string TestSource;
+            public readonly string TestBin;
+            public readonly ITestOutputHelper Output;
+        }
+
+        // Fill 'Tags' dictionary with tags lines
+        Dictionary<string, int> CollectTags(string srcName)
+        {
+            Dictionary<string, int> Tags = new Dictionary<string, int>();
+            int lineCounter = 0;
+            string[] separators = new string[] {"//"};
+            string pattern = @".*@([^@]+)@.*";
+            Regex reg = new Regex (pattern);
+
+            foreach (string line in File.ReadLines(srcName))
+            {
+                lineCounter++;
+
+                Match match = reg.Match(line);
+
+                if (!match.Success)
+                    continue;
+
+                string key = match.Groups[1].ToString().Trim();
+                if (Tags.ContainsKey(key))
+                    throw new Exception(String.Format("Tag '{0}' presented more than once in file '{1}'", key, srcName));
+                Tags[key] = lineCounter;
+            }
+
+            return Tags;
+        }
+
+        private class CommentCollector : CSharpSyntaxRewriter
+        {
+            private StringBuilder allComments = new StringBuilder();
+            private int lineCount = 0;
+            public override SyntaxTrivia VisitTrivia(SyntaxTrivia trivia)
+            {
+                if (!trivia.IsKind(SyntaxKind.SingleLineCommentTrivia) && !trivia.IsKind(SyntaxKind.MultiLineCommentTrivia))
+                    return trivia;
+
+                var lineSpan = trivia.GetLocation().GetLineSpan();
+
+                while (lineCount < lineSpan.StartLinePosition.Line)
+                {
+                    allComments.AppendLine();
+                    lineCount++;
+                }
+                string comment = trivia.ToString();
+                if (trivia.IsKind(SyntaxKind.SingleLineCommentTrivia))
+                    comment = comment.Substring(2);
+                else if (trivia.IsKind(SyntaxKind.MultiLineCommentTrivia))
+                    comment = comment.Substring(2, comment.Length - 4);
+
+                allComments.Append(comment);
+                lineCount = lineSpan.EndLinePosition.Line;
+
+                return trivia;
+            }
+            public string Text { get => allComments.ToString(); }
+        }
+
+        private readonly ITestOutputHelper output;
+        private string debugger;
+        public TestRunner(ITestOutputHelper output)
+        {
+            this.output = output;
+
+            // Sneaky way to get assembly path, which works even if call
+            // current constructor with reflection
+            string codeBase = Assembly.GetExecutingAssembly().CodeBase;
+            UriBuilder uri = new UriBuilder(codeBase);
+            string path = Uri.UnescapeDataString(uri.Path);
+            var d = new DirectoryInfo(Path.GetDirectoryName(path));
+
+            // Get path to runner binaries
+            this.debugger = Path.Combine(d.Parent.Parent.Parent.Parent.Parent.FullName, "bin", "netcoredbg");
+        }
+
+        [Theory]
+        [MemberData(nameof(Data))]
+        public void ExecuteTest(Labeled<(string binName, string srcName)> t)
+        {
+            var lines = CollectTags(t.Data.srcName);
+
+            var tree = CSharpSyntaxTree.ParseText(File.ReadAllText(t.Data.srcName))
+                                       .WithFilePath(t.Data.srcName);
+
+            var cc = new CommentCollector();
+            cc.Visit(tree.GetRoot());
+
+            output.WriteLine("------ Test script ------");
+            output.WriteLine(cc.Text);
+            output.WriteLine("-------------------------");
+
+            var script = CSharpScript.Create(
+                cc.Text,
+                ScriptOptions.Default.WithReferences(typeof(object).Assembly)
+                                     .WithReferences(typeof(Xunit.Assert).Assembly)
+                                     .WithImports("System")
+                                     .WithImports("Xunit"),
+                globalsType: typeof(TestCaseGlobals)
+            );
+            script.Compile();
+
+            ProcessInfo processInfo = new ProcessInfo(debugger, output);
+
+            // Globals, to use inside test case
+            TestCaseGlobals globals = new TestCaseGlobals(
+                processInfo,
+                lines,
+                t.Data.srcName,
+                t.Data.binName,
+                output
+            );
+
+            script.RunAsync(globals).Wait();
+
+            // Finish process
+            processInfo.Close();
+        }
+    }
+}
diff --git a/tests/runner/runner.csproj b/tests/runner/runner.csproj
new file mode 100644 (file)
index 0000000..861857e
--- /dev/null
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netcoreapp2.0</TargetFramework>
+    <OutputType>Library</OutputType>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0" />
+    <PackageReference Include="System.Threading.Tasks.Dataflow" Version="4.8.0" />
+    <PackageReference Include="xunit" Version="2.2.0" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
+    <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="2.4.0" />
+    <PackageReference Include="Microsoft.CodeAnalysis.Scripting" Version="2.4.0" />
+  </ItemGroup>
+
+</Project>
diff --git a/tests/simple_stepping/Program.cs b/tests/simple_stepping/Program.cs
new file mode 100644 (file)
index 0000000..91c282d
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+Send("1-file-exec-and-symbols dotnet");
+Send("2-exec-arguments " + TestBin);
+Send("3-exec-run");
+
+var r = Expect("*stopped");
+Assert.Equal(r.FindString("reason"), "entry-point-hit");
+Assert.Equal(r.Find("frame").FindInt("line"), Lines["START"]);
+
+Send("4-exec-step");
+r = Expect("*stopped");
+Assert.Equal(r.FindString("reason"), "end-stepping-range");
+Assert.Equal(r.Find("frame").FindInt("line"), Lines["STEP1"]);
+
+Send("5-exec-step");
+r = Expect("*stopped");
+Assert.Equal(r.FindString("reason"), "end-stepping-range");
+Assert.Equal(r.Find("frame").FindInt("line"), Lines["STEP2"]);
+
+Send("6-gdb-exit");
+*/
+
+using System;
+
+namespace simple_stepping
+{
+    class Program
+    {
+        static void Main(string[] args)
+        {                                      // //@START@
+            Console.WriteLine("Hello World!"); // //@STEP1@
+        }                                      // //@STEP2@
+    }
+}
diff --git a/tests/simple_stepping/simple_stepping.csproj b/tests/simple_stepping/simple_stepping.csproj
new file mode 100644 (file)
index 0000000..ce1697a
--- /dev/null
@@ -0,0 +1,8 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>netcoreapp2.0</TargetFramework>
+  </PropertyGroup>
+
+</Project>
diff --git a/tests/values/Program.cs b/tests/values/Program.cs
new file mode 100644 (file)
index 0000000..1f10ef1
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+using System;
+
+Send("1-file-exec-and-symbols dotnet");
+Send("2-exec-arguments " + TestBin);
+Send("3-exec-run");
+
+var r = Expect("*stopped");
+Assert.Equal("entry-point-hit", r.FindString("reason"));
+Assert.Equal(Lines["START"], r.Find("frame").FindInt("line"));
+
+Send(String.Format("4-break-insert -f {0}:{1}", TestSource, Lines["BREAK"]));
+r = Expect("4^done");
+
+Send("5-exec-continue");
+*/
+
+using System;
+
+namespace values
+{
+    class Program
+    {
+        static void Main(string[] args)
+        {              // //@START@
+            decimal d = 12345678901234567890123456m;
+            int x = 1; // //@BREAK@
+            /*
+r = Expect("*stopped");
+Assert.Equal("breakpoint-hit", r.FindString("reason"));
+Assert.Equal(Lines["BREAK"], r.Find("frame").FindInt("line"));
+
+Send(String.Format("6-var-create - * \"{0}\"", "d"));
+r = Expect("6^done");
+Assert.Equal("12345678901234567890123456", r.FindString("value"));
+Assert.Equal("d", r.FindString("exp"));
+Assert.Equal("0", r.FindString("numchild"));
+Assert.Equal("decimal", r.FindString("type"));
+             */
+        }
+    }
+}
+/*
+Send("7-exec-continue");
+r = Expect("*stopped");
+Assert.Equal("exited", r.FindString("reason"));
+*/
diff --git a/tests/values/values.csproj b/tests/values/values.csproj
new file mode 100644 (file)
index 0000000..ce1697a
--- /dev/null
@@ -0,0 +1,8 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>netcoreapp2.0</TargetFramework>
+  </PropertyGroup>
+
+</Project>