diff --git a/Tests/Connector/Reqnroll.VisualStudio.ReqnrollConnector.Tests/ConnectorSampleTests.cs b/Tests/Connector/Reqnroll.VisualStudio.ReqnrollConnector.Tests/ConnectorSampleTests.cs
new file mode 100644
index 00000000..e2a36519
--- /dev/null
+++ b/Tests/Connector/Reqnroll.VisualStudio.ReqnrollConnector.Tests/ConnectorSampleTests.cs
@@ -0,0 +1,15 @@
+namespace Reqnroll.VisualStudio.ReqnrollConnector.Tests;
+
+public class ConnectorSampleTests : SampleProjectTestBase
+{
+ public ConnectorSampleTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper)
+ {
+ }
+
+ [Theory]
+ [MemberData(nameof(GetProjectsForRepository), @"Tests\VsExtConnectorTestSamples", "")]
+ public void All(string testCase, string projectFile, string repositoryDirectory)
+ {
+ ValidateProject(testCase, projectFile, repositoryDirectory);
+ }
+}
\ No newline at end of file
diff --git a/Tests/Connector/Reqnroll.VisualStudio.ReqnrollConnector.Tests/ExternalSampleTests.cs b/Tests/Connector/Reqnroll.VisualStudio.ReqnrollConnector.Tests/ExternalSampleTests.cs
index 5f21df41..cd5e3502 100644
--- a/Tests/Connector/Reqnroll.VisualStudio.ReqnrollConnector.Tests/ExternalSampleTests.cs
+++ b/Tests/Connector/Reqnroll.VisualStudio.ReqnrollConnector.Tests/ExternalSampleTests.cs
@@ -1,23 +1,13 @@
-using System.Text.RegularExpressions;
-using Reqnroll.VisualStudio.ReqnrollConnector.Models;
-
namespace Reqnroll.VisualStudio.ReqnrollConnector.Tests;
///
/// This test class validates whether connector can work with various sample projects from external repositories.
/// It clones/pulls the repository to a temp folder, builds each Reqnroll project, and runs discovery using the connector.
///
-public class ExternalSampleTests
+public class ExternalSampleTests : SampleProjectTestBase
{
- private const string ConnectorConfiguration = "Debug";
- private const string TargetFrameworkToBeUsedForNet4Projects = "net10.0";
- private static readonly string LatestReqnrollVersion = NuGetPackageVersionDetector.DetectLatestPackage("Reqnroll", Console.WriteLine) ?? "1.0.0";
-
- protected readonly ITestOutputHelper TestOutputHelper;
-
- public ExternalSampleTests(ITestOutputHelper testOutputHelper)
+ public ExternalSampleTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper)
{
- TestOutputHelper = testOutputHelper;
}
[Theory]
@@ -44,283 +34,4 @@ public void ExploratoryTestProjects(string testCase, string projectFile, string
//{
// ValidateProject(testCase, projectFile, repositoryDirectory);
//}
-
- protected void ValidateProject(string testCase, string projectFile, string repositoryDirectory)
- {
- TestOutputHelper.WriteLine("Running Test Case: " + testCase);
- var fullProjectPath = Path.Combine(repositoryDirectory, projectFile);
- BuildAndInspectProject(repositoryDirectory, fullProjectPath);
- }
-
- private void BuildAndInspectProject(string repositoryDirectory, string projectFile)
- {
- var projectDirectory = Path.GetDirectoryName(projectFile)!;
- bool isPackagesStyleProject = File.Exists(Path.Combine(projectDirectory, "packages.config"));
-
- if (isPackagesStyleProject)
- {
- TestOutputHelper.WriteLine($"Restore packages.config dependencies for {projectFile}");
- var solutionFolder = Path.GetFullPath(Path.Combine(projectDirectory, ".."));
- string nugetPath = Path.Combine(solutionFolder, "nuget.exe");
- if (!File.Exists(nugetPath))
- {
- RunProcess(solutionFolder, "curl", "-o nuget.exe https://dist.nuget.org/win-x86-commandline/latest/nuget.exe");
- }
- RunProcess(solutionFolder, nugetPath, "restore");
- }
-
- TestOutputHelper.WriteLine($"Building {projectFile}");
- RunProcess(repositoryDirectory, "dotnet", $"build \"{projectFile}\"");
-
- var projectName = Path.GetFileNameWithoutExtension(projectFile);
- var configPath = GetConfigFile(projectDirectory);
- TestOutputHelper.WriteLine(configPath is null ? "No config file found" : $"Config: {configPath}");
-
- var debugDirectory = Path.Combine(projectDirectory, "bin", "Debug");
- Directory.Exists(debugDirectory).Should().BeTrue($"Build output folder not found for {projectFile}");
-
- var assembliesToCheck = isPackagesStyleProject
- ? GetAssembliesToCheckPackagesStyle()
- : GetAssembliesToCheckSdkStyle();
-
- (string targetFramework, string? assemblyPath)[] GetAssembliesToCheckSdkStyle()
- {
- var targetFrameworkDirectories = Directory.EnumerateDirectories(debugDirectory).ToArray();
- targetFrameworkDirectories.Should().NotBeEmpty($"No target frameworks found under {debugDirectory}");
- return targetFrameworkDirectories.Select(tfmDirectory =>
- {
- var targetFramework = Path.GetFileName(tfmDirectory);
- var assemblyPath = FindAssembly(tfmDirectory, projectName);
- return (targetFramework, assemblyPath);
- }).ToArray();
- }
-
- (string targetFramework, string? assemblyPath)[] GetAssembliesToCheckPackagesStyle()
- {
- var projectFileContent = File.ReadAllText(projectFile);
- var targetFrameworkMatch = Regex.Match(projectFileContent, "v(?\\d+\\.\\d+.\\d+)");
- targetFrameworkMatch.Success.Should().BeTrue($"Cannot determine target framework from {projectFile}");
- var targetFramework = "net" + targetFrameworkMatch.Groups["tfm"].Value.Replace(".", string.Empty);
- var assemblyPath = FindAssembly(debugDirectory, projectName);
- return new[] { (targetFramework, assemblyPath) };
- }
-
- foreach (var (targetFramework, assemblyPath) in assembliesToCheck)
- {
- assemblyPath.Should().NotBeNull($"Cannot find assembly {projectName}.dll under {Path.GetDirectoryName(assemblyPath)}");
-
- TestOutputHelper.WriteLine($"{targetFramework}: {assemblyPath}");
- CheckConnector(targetFramework, assemblyPath, configPath);
- }
-
- static string? FindAssembly(string tfmDirectory, string projectName)
- {
- var candidate = Path.Combine(tfmDirectory, $"{projectName}.dll");
- if (File.Exists(candidate))
- return candidate;
-
- return Directory.EnumerateFiles(tfmDirectory, $"{projectName}.dll", SearchOption.AllDirectories).FirstOrDefault();
- }
- }
-
- private void CheckConnector(string targetFramework, string assemblyPath, string? configPath)
- {
- var connectorPath = GetConnectorPath(targetFramework);
- File.Exists(connectorPath).Should().BeTrue($"Connector not found: {connectorPath}");
-
- var configArgument = configPath ?? string.Empty;
- var args = $"exec \"{connectorPath}\" discovery \"{assemblyPath}\" \"{configArgument}\"";
- var result = RunProcess(Path.GetDirectoryName(assemblyPath)!, "dotnet", args);
-
- if (!string.IsNullOrEmpty(result.StdError))
- {
- TestOutputHelper.WriteLine("Connector error output:");
- TestOutputHelper.WriteLine(result.StdError);
- }
-
- var discoveryResult = ExtractDiscoveryResult(result.StdOutput);
-
- if (discoveryResult.IsFailed)
- {
- TestOutputHelper.WriteLine("Connector output:");
- TestOutputHelper.WriteLine(result.StdOutput);
- }
-
- discoveryResult.ConnectorType.Should().NotBeEmpty();
- discoveryResult.ReqnrollVersion.Should().NotBeEmpty();
- discoveryResult.ErrorMessage.Should().BeNullOrEmpty();
- discoveryResult.StepDefinitions.Should().NotBeEmpty();
- discoveryResult.SourceFiles.Should().NotBeEmpty();
-
- discoveryResult.AnalyticsProperties.Should().NotBeNull();
- discoveryResult.AnalyticsProperties.Should().ContainKeys(
- "Connector",
- "ImageRuntimeVersion",
- "TargetFramework",
- "SFFile",
- "SFFileVersion",
- "SFProductVersion",
- "TypeNames",
- "SourcePaths",
- "StepDefinitions",
- "Hooks");
-
- discoveryResult.Warnings.Should().BeNullOrEmpty();
- }
-
- public static TheoryData GetProjectsForRepository(string repositoryUrl, string excludedFolders)
- {
- var theoryData = new TheoryData();
-
- var repositoryDirectory = PrepareRepository(repositoryUrl);
- var repositoryName = GetRepositoryNameFromUrl(repositoryUrl);
- var excludeFoldersList = string.IsNullOrEmpty(excludedFolders) ? Array.Empty() : excludedFolders.Split(';').Where(folder => !string.IsNullOrWhiteSpace(folder)).ToArray();
-
- var projectsWithFeatures = Directory
- .EnumerateFiles(repositoryDirectory, "*.*proj", SearchOption.AllDirectories)
- .Where(projectFile =>
- {
- var projectDirectory = Path.GetDirectoryName(projectFile)!;
- if (excludeFoldersList.Any(exclude => projectDirectory.Contains(exclude)))
- return false;
- return Directory.EnumerateFiles(projectDirectory, "*.feature", SearchOption.AllDirectories).Any();
- })
- .ToArray();
-
- foreach (var projectFile in projectsWithFeatures)
- {
- var testDisplayName = $"{Path.GetFileNameWithoutExtension(projectFile)} in {repositoryName}";
- var relativeProjectFile = Path.GetRelativePath(repositoryDirectory, projectFile);
- theoryData.Add(testDisplayName, relativeProjectFile, repositoryDirectory);
- Console.WriteLine($" Test added: {testDisplayName} ({relativeProjectFile})");
- }
-
- Console.WriteLine($"Total tests added: {projectsWithFeatures.Length}");
-
- return theoryData;
- }
-
- internal static string PrepareRepository(string repositoryUrl)
- {
- if (!repositoryUrl.StartsWith("https://"))
- {
- // assume folder (absolute or relative to the solution root)
- var localRepositoryDirectory =
- Path.GetFullPath(Path.Combine(GetSolutionRoot(), repositoryUrl));
- Directory.Exists(localRepositoryDirectory).Should().BeTrue($"Repository folder not found: {localRepositoryDirectory}");
- return localRepositoryDirectory;
- }
-
- var repositoryName = GetRepositoryNameFromUrl(repositoryUrl);
- var tempRootDirectory = Path.Combine(Path.GetTempPath(), "ReqnrollSamples");
- Directory.CreateDirectory(tempRootDirectory);
-
- var repositoryDirectory = Path.Combine(tempRootDirectory, repositoryName);
-
- if (!Directory.Exists(repositoryDirectory))
- {
- RunProcessStatic(tempRootDirectory, "git", $"clone {repositoryUrl} \"{repositoryDirectory}\"");
- }
- else
- {
- RunProcessStatic(repositoryDirectory, "git", "reset --hard");
- RunProcessStatic(repositoryDirectory, "git", "clean -fd");
- RunProcessStatic(repositoryDirectory, "git", "pull");
- }
-
- var updateScript = Path.Combine(repositoryDirectory, "update-versions.ps1");
- if (File.Exists(updateScript))
- {
- RunProcessStatic(repositoryDirectory, "powershell", $"-ExecutionPolicy Bypass -File \"{updateScript}\" {LatestReqnrollVersion}");
- }
-
- return repositoryDirectory;
- }
-
- internal static string GetRepositoryNameFromUrl(string repositoryUrl)
- {
- if (!repositoryUrl.StartsWith("https://"))
- {
- return repositoryUrl.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar).Last();
- }
-
- var uri = new Uri(repositoryUrl);
- var lastSegment = uri.Segments.Last().Trim('/');
- if (lastSegment.EndsWith(".git", StringComparison.OrdinalIgnoreCase))
- lastSegment = lastSegment[..^4];
-
- return lastSegment;
- }
-
- private static string GetConnectorPath(string targetFramework)
- {
- if (targetFramework.StartsWith("net4"))
- targetFramework = TargetFrameworkToBeUsedForNet4Projects;
-
- var connectorDir = Path.Combine(GetSolutionRoot(), "Connectors", "bin", ConnectorConfiguration, $"Reqnroll-Generic-{targetFramework}");
- return Path.Combine(connectorDir, "reqnroll-vs.dll");
- }
-
- private static string GetSolutionRoot()
- {
- var assemblyLocation = Assembly.GetExecutingAssembly().Location;
- var testAssemblyDir = Path.GetDirectoryName(assemblyLocation)!;
- var solutionRoot = Path.GetFullPath(Path.Combine(testAssemblyDir, "..", "..", "..", "..", "..", ".."));
- return solutionRoot;
- }
-
- private static DiscoveryResult ExtractDiscoveryResult(string stdOutput)
- {
- var json = ExtractJson(stdOutput);
- var deserialized = TestBase.DeserializeObject(json);
- deserialized.Should().NotBeNull($"Cannot deserialize discovery result: {json}");
- return deserialized;
- }
-
- private static string ExtractJson(string stdOutput)
- {
- const string startMarker = ">>>>>>>>>>";
- const string endMarker = "<<<<<<<<<<";
- var start = stdOutput.IndexOf(startMarker, StringComparison.Ordinal);
- if (start >= 0)
- {
- start += startMarker.Length;
- var end = stdOutput.IndexOf(endMarker, start, StringComparison.Ordinal);
- if (end >= 0)
- return stdOutput.Substring(start, end - start).Trim();
- }
-
- return stdOutput.Trim();
- }
-
- private static string? GetConfigFile(string projectDirectory)
- {
- var reqnrollConfig = Path.Combine(projectDirectory, "reqnroll.json");
- if (File.Exists(reqnrollConfig))
- return reqnrollConfig;
-
- var appConfig = Path.Combine(projectDirectory, "App.config");
- return File.Exists(appConfig) ? appConfig : null;
- }
-
- private static ProcessResult RunProcessInternal(string workingDirectory, string executablePath, string arguments, Action logResult)
- {
- logResult($"{workingDirectory}> {executablePath} {arguments}");
- var result = new ProcessHelper().RunProcess(new ProcessStartInfoEx(workingDirectory, executablePath, arguments));
-
- if (!string.IsNullOrWhiteSpace(result.StdOutput) && result.ExitCode != 0)
- logResult(result.StdOutput);
-
- if (!string.IsNullOrWhiteSpace(result.StdError))
- logResult(result.StdError);
-
- result.ExitCode.Should().Be(0, $"command failed: {executablePath} {arguments}");
- return result;
- }
-
- private ProcessResult RunProcess(string workingDirectory, string executablePath, string arguments)
- => RunProcessInternal(workingDirectory, executablePath, arguments, TestOutputHelper.WriteLine);
-
- private static void RunProcessStatic(string workingDirectory, string executablePath, string arguments)
- => RunProcessInternal(workingDirectory, executablePath, arguments, Console.WriteLine);
}
diff --git a/Tests/Connector/Reqnroll.VisualStudio.ReqnrollConnector.Tests/SampleProjectTestBase.cs b/Tests/Connector/Reqnroll.VisualStudio.ReqnrollConnector.Tests/SampleProjectTestBase.cs
new file mode 100644
index 00000000..cad052a2
--- /dev/null
+++ b/Tests/Connector/Reqnroll.VisualStudio.ReqnrollConnector.Tests/SampleProjectTestBase.cs
@@ -0,0 +1,297 @@
+using Reqnroll.VisualStudio.ReqnrollConnector.Models;
+using System.Text.RegularExpressions;
+
+namespace Reqnroll.VisualStudio.ReqnrollConnector.Tests;
+
+public abstract class SampleProjectTestBase
+{
+ private const string ConnectorConfiguration = "Debug";
+ private const string TargetFrameworkToBeUsedForNet4Projects = "net10.0";
+ private static readonly string LatestReqnrollVersion = NuGetPackageVersionDetector.DetectLatestPackage("Reqnroll", Console.WriteLine) ?? "1.0.0";
+
+ protected readonly ITestOutputHelper TestOutputHelper;
+
+ protected SampleProjectTestBase(ITestOutputHelper testOutputHelper)
+ {
+ TestOutputHelper = testOutputHelper;
+ }
+
+ protected void ValidateProject(string testCase, string projectFile, string repositoryDirectory)
+ {
+ TestOutputHelper.WriteLine("Running Test Case: " + testCase);
+ var fullProjectPath = Path.Combine(repositoryDirectory, projectFile);
+ BuildAndInspectProject(repositoryDirectory, fullProjectPath);
+ }
+
+ private void BuildAndInspectProject(string repositoryDirectory, string projectFile)
+ {
+ var projectDirectory = Path.GetDirectoryName(projectFile)!;
+ bool isPackagesStyleProject = File.Exists(Path.Combine(projectDirectory, "packages.config"));
+
+ if (isPackagesStyleProject)
+ {
+ TestOutputHelper.WriteLine($"Restore packages.config dependencies for {projectFile}");
+ var solutionFolder = Path.GetFullPath(Path.Combine(projectDirectory, ".."));
+ string nugetPath = Path.Combine(solutionFolder, "nuget.exe");
+ if (!File.Exists(nugetPath))
+ {
+ RunProcess(solutionFolder, "curl", "-o nuget.exe https://dist.nuget.org/win-x86-commandline/latest/nuget.exe");
+ }
+ RunProcess(solutionFolder, nugetPath, "restore");
+ }
+
+ TestOutputHelper.WriteLine($"Building {projectFile}");
+ RunProcess(repositoryDirectory, "dotnet", $"build \"{projectFile}\"");
+
+ var projectName = Path.GetFileNameWithoutExtension(projectFile);
+ var configPath = GetConfigFile(projectDirectory);
+ TestOutputHelper.WriteLine(configPath is null ? "No config file found" : $"Config: {configPath}");
+
+ var debugDirectory = Path.Combine(projectDirectory, "bin", "Debug");
+ Directory.Exists(debugDirectory).Should().BeTrue($"Build output folder not found for {projectFile}");
+
+ var assembliesToCheck = isPackagesStyleProject
+ ? GetAssembliesToCheckPackagesStyle()
+ : GetAssembliesToCheckSdkStyle();
+
+ (string targetFramework, string? assemblyPath)[] GetAssembliesToCheckSdkStyle()
+ {
+ var targetFrameworkDirectories = Directory.EnumerateDirectories(debugDirectory).ToArray();
+ targetFrameworkDirectories.Should().NotBeEmpty($"No target frameworks found under {debugDirectory}");
+ return targetFrameworkDirectories.Select(tfmDirectory =>
+ {
+ var targetFramework = Path.GetFileName(tfmDirectory);
+ var assemblyPath = FindAssembly(tfmDirectory, projectName);
+ return (targetFramework, assemblyPath);
+ }).ToArray();
+ }
+
+ (string targetFramework, string? assemblyPath)[] GetAssembliesToCheckPackagesStyle()
+ {
+ var projectFileContent = File.ReadAllText(projectFile);
+ var targetFrameworkMatch = Regex.Match(projectFileContent, "v(?\\d+\\.\\d+.\\d+)");
+ targetFrameworkMatch.Success.Should().BeTrue($"Cannot determine target framework from {projectFile}");
+ var targetFramework = "net" + targetFrameworkMatch.Groups["tfm"].Value.Replace(".", string.Empty);
+ var assemblyPath = FindAssembly(debugDirectory, projectName);
+ return new[] { (targetFramework, assemblyPath) };
+ }
+
+ foreach (var (targetFramework, assemblyPath) in assembliesToCheck)
+ {
+ assemblyPath.Should().NotBeNull($"Cannot find assembly {projectName}.dll under {Path.GetDirectoryName(assemblyPath)}");
+
+ TestOutputHelper.WriteLine($"{targetFramework}: {assemblyPath}");
+ CheckConnector(targetFramework, assemblyPath, configPath);
+ }
+
+ static string? FindAssembly(string tfmDirectory, string projectName)
+ {
+ var candidate = Path.Combine(tfmDirectory, $"{projectName}.dll");
+ if (File.Exists(candidate))
+ return candidate;
+
+ return Directory.EnumerateFiles(tfmDirectory, $"{projectName}.dll", SearchOption.AllDirectories).FirstOrDefault();
+ }
+ }
+
+ private void CheckConnector(string targetFramework, string assemblyPath, string? configPath)
+ {
+ var connectorPath = GetConnectorPath(targetFramework);
+ File.Exists(connectorPath).Should().BeTrue($"Connector not found: {connectorPath}");
+
+ var configArgument = configPath ?? string.Empty;
+ var args = $"exec \"{connectorPath}\" discovery \"{assemblyPath}\" \"{configArgument}\"";
+ var result = RunProcess(Path.GetDirectoryName(assemblyPath)!, "dotnet", args);
+
+ if (!string.IsNullOrEmpty(result.StdError))
+ {
+ TestOutputHelper.WriteLine("Connector error output:");
+ TestOutputHelper.WriteLine(result.StdError);
+ }
+
+ var discoveryResult = ExtractDiscoveryResult(result.StdOutput);
+
+ if (discoveryResult.IsFailed)
+ {
+ TestOutputHelper.WriteLine("Connector output:");
+ TestOutputHelper.WriteLine(result.StdOutput);
+ }
+
+ discoveryResult.ConnectorType.Should().NotBeEmpty();
+ discoveryResult.ReqnrollVersion.Should().NotBeEmpty();
+ discoveryResult.ErrorMessage.Should().BeNullOrEmpty();
+ discoveryResult.StepDefinitions.Should().NotBeEmpty();
+ discoveryResult.SourceFiles.Should().NotBeEmpty();
+
+ discoveryResult.AnalyticsProperties.Should().NotBeNull();
+ discoveryResult.AnalyticsProperties.Should().ContainKeys(
+ "Connector",
+ "ImageRuntimeVersion",
+ "TargetFramework",
+ "SFFile",
+ "SFFileVersion",
+ "SFProductVersion",
+ "TypeNames",
+ "SourcePaths",
+ "StepDefinitions",
+ "Hooks");
+
+ discoveryResult.Warnings.Should().BeNullOrEmpty();
+ }
+
+ public static TheoryData GetProjectsForRepository(string repositoryUrl, string excludedFolders)
+ {
+ var theoryData = new TheoryData();
+
+ var repositoryDirectory = PrepareRepository(repositoryUrl);
+ var repositoryName = GetRepositoryNameFromUrl(repositoryUrl);
+ var excludeFoldersList = string.IsNullOrEmpty(excludedFolders) ? Array.Empty() : excludedFolders.Split(';').Where(folder => !string.IsNullOrWhiteSpace(folder)).ToArray();
+
+ var projectsWithFeatures = Directory
+ .EnumerateFiles(repositoryDirectory, "*.*proj", SearchOption.AllDirectories)
+ .Where(projectFile =>
+ {
+ var projectDirectory = Path.GetDirectoryName(projectFile)!;
+ if (excludeFoldersList.Any(exclude => projectDirectory.Contains(exclude)))
+ return false;
+ return Directory.EnumerateFiles(projectDirectory, "*.feature", SearchOption.AllDirectories).Any();
+ })
+ .ToArray();
+
+ foreach (var projectFile in projectsWithFeatures)
+ {
+ var testDisplayName = $"{Path.GetFileNameWithoutExtension(projectFile)} in {repositoryName}";
+ var relativeProjectFile = Path.GetRelativePath(repositoryDirectory, projectFile);
+ theoryData.Add(testDisplayName, relativeProjectFile, repositoryDirectory);
+ Console.WriteLine($" Test added: {testDisplayName} ({relativeProjectFile})");
+ }
+
+ Console.WriteLine($"Total tests added: {projectsWithFeatures.Length}");
+
+ return theoryData;
+ }
+
+ internal static string PrepareRepository(string repositoryUrl)
+ {
+ if (!repositoryUrl.StartsWith("https://"))
+ {
+ // assume folder (absolute or relative to the solution root)
+ var localRepositoryDirectory =
+ Path.GetFullPath(Path.Combine(GetSolutionRoot(), repositoryUrl));
+ Directory.Exists(localRepositoryDirectory).Should().BeTrue($"Repository folder not found: {localRepositoryDirectory}");
+ return localRepositoryDirectory;
+ }
+
+ var repositoryName = GetRepositoryNameFromUrl(repositoryUrl);
+ var tempRootDirectory = Path.Combine(Path.GetTempPath(), "ReqnrollSamples");
+ Directory.CreateDirectory(tempRootDirectory);
+
+ var repositoryDirectory = Path.Combine(tempRootDirectory, repositoryName);
+
+ if (!Directory.Exists(repositoryDirectory))
+ {
+ RunProcessStatic(tempRootDirectory, "git", $"clone {repositoryUrl} \"{repositoryDirectory}\"");
+ }
+ else
+ {
+ RunProcessStatic(repositoryDirectory, "git", "reset --hard");
+ RunProcessStatic(repositoryDirectory, "git", "clean -fd");
+ RunProcessStatic(repositoryDirectory, "git", "pull");
+ }
+
+ var updateScript = Path.Combine(repositoryDirectory, "update-versions.ps1");
+ if (File.Exists(updateScript))
+ {
+ RunProcessStatic(repositoryDirectory, "powershell", $"-ExecutionPolicy Bypass -File \"{updateScript}\" {LatestReqnrollVersion}");
+ }
+
+ return repositoryDirectory;
+ }
+
+ internal static string GetRepositoryNameFromUrl(string repositoryUrl)
+ {
+ if (!repositoryUrl.StartsWith("https://"))
+ {
+ return repositoryUrl.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar).Last();
+ }
+
+ var uri = new Uri(repositoryUrl);
+ var lastSegment = uri.Segments.Last().Trim('/');
+ if (lastSegment.EndsWith(".git", StringComparison.OrdinalIgnoreCase))
+ lastSegment = lastSegment[..^4];
+
+ return lastSegment;
+ }
+
+ private static string GetConnectorPath(string targetFramework)
+ {
+ if (targetFramework.StartsWith("net4"))
+ targetFramework = TargetFrameworkToBeUsedForNet4Projects;
+
+ var connectorDir = Path.Combine(GetSolutionRoot(), "Connectors", "bin", ConnectorConfiguration, $"Reqnroll-Generic-{targetFramework}");
+ return Path.Combine(connectorDir, "reqnroll-vs.dll");
+ }
+
+ private static string GetSolutionRoot()
+ {
+ var assemblyLocation = Assembly.GetExecutingAssembly().Location;
+ var testAssemblyDir = Path.GetDirectoryName(assemblyLocation)!;
+ var solutionRoot = Path.GetFullPath(Path.Combine(testAssemblyDir, "..", "..", "..", "..", "..", ".."));
+ return solutionRoot;
+ }
+
+ private static DiscoveryResult ExtractDiscoveryResult(string stdOutput)
+ {
+ var json = ExtractJson(stdOutput);
+ var deserialized = TestBase.DeserializeObject(json);
+ deserialized.Should().NotBeNull($"Cannot deserialize discovery result: {json}");
+ return deserialized;
+ }
+
+ private static string ExtractJson(string stdOutput)
+ {
+ const string startMarker = ">>>>>>>>>>";
+ const string endMarker = "<<<<<<<<<<";
+ var start = stdOutput.IndexOf(startMarker, StringComparison.Ordinal);
+ if (start >= 0)
+ {
+ start += startMarker.Length;
+ var end = stdOutput.IndexOf(endMarker, start, StringComparison.Ordinal);
+ if (end >= 0)
+ return stdOutput.Substring(start, end - start).Trim();
+ }
+
+ return stdOutput.Trim();
+ }
+
+ private static string? GetConfigFile(string projectDirectory)
+ {
+ var reqnrollConfig = Path.Combine(projectDirectory, "reqnroll.json");
+ if (File.Exists(reqnrollConfig))
+ return reqnrollConfig;
+
+ var appConfig = Path.Combine(projectDirectory, "App.config");
+ return File.Exists(appConfig) ? appConfig : null;
+ }
+
+ private static ProcessResult RunProcessInternal(string workingDirectory, string executablePath, string arguments, Action logResult)
+ {
+ logResult($"{workingDirectory}> {executablePath} {arguments}");
+ var result = new ProcessHelper().RunProcess(new ProcessStartInfoEx(workingDirectory, executablePath, arguments));
+
+ if (!string.IsNullOrWhiteSpace(result.StdOutput) && result.ExitCode != 0)
+ logResult(result.StdOutput);
+
+ if (!string.IsNullOrWhiteSpace(result.StdError))
+ logResult(result.StdError);
+
+ result.ExitCode.Should().Be(0, $"command failed: {executablePath} {arguments}");
+ return result;
+ }
+
+ private ProcessResult RunProcess(string workingDirectory, string executablePath, string arguments)
+ => RunProcessInternal(workingDirectory, executablePath, arguments, TestOutputHelper.WriteLine);
+
+ private static void RunProcessStatic(string workingDirectory, string executablePath, string arguments)
+ => RunProcessInternal(workingDirectory, executablePath, arguments, Console.WriteLine);
+}
\ No newline at end of file
diff --git a/Tests/VsExtConnectorTestSamples/VsExtConnectorTestSamples.ReqnrollCalculator.Tests/Calculator.cs b/Tests/VsExtConnectorTestSamples/VsExtConnectorTestSamples.ReqnrollCalculator.Tests/Calculator.cs
new file mode 100644
index 00000000..5795b6f0
--- /dev/null
+++ b/Tests/VsExtConnectorTestSamples/VsExtConnectorTestSamples.ReqnrollCalculator.Tests/Calculator.cs
@@ -0,0 +1,36 @@
+namespace ReqnrollCalculator;
+
+public class Calculator
+{
+ private readonly Stack _numbers = new();
+
+ public void Enter(int number)
+ {
+ _numbers.Push(number);
+ }
+
+ public void Reset()
+ {
+ _numbers.Clear();
+ }
+
+ public int GetResult()
+ {
+ return _numbers.Peek();
+ }
+
+ public string GetResultMessage()
+ {
+ return $"The result is {GetResult()}.";
+ }
+
+ public void Add()
+ {
+ _numbers.Push(_numbers.Pop() + _numbers.Pop());
+ }
+
+ public void Multiply()
+ {
+ _numbers.Push(_numbers.Pop() * _numbers.Pop());
+ }
+}
\ No newline at end of file
diff --git a/Tests/VsExtConnectorTestSamples/VsExtConnectorTestSamples.ReqnrollCalculator.Tests/Features/Addition.feature b/Tests/VsExtConnectorTestSamples/VsExtConnectorTestSamples.ReqnrollCalculator.Tests/Features/Addition.feature
new file mode 100644
index 00000000..39a31814
--- /dev/null
+++ b/Tests/VsExtConnectorTestSamples/VsExtConnectorTestSamples.ReqnrollCalculator.Tests/Features/Addition.feature
@@ -0,0 +1,53 @@
+Feature: Addition
+
+The calculator functions related to addition,
+that is denoted with the '+' operator.
+
+@core
+Rule: Positive numbers can be added
+
+Scenario: Add two numbers
+ This is the most common use-case:
+ people often sum two numbers.
+
+ Given the first number is 50
+ And the second number is 70
+ When the two numbers are added
+ Then the result should be 120
+
+@commutativity
+Scenario: Addition is commutative
+ Given the first number is 70
+ And the second number is 50
+ When the two numbers are added
+ Then the result should be 120
+
+Rule: Non-positive numbers can be added
+
+@edgeCase
+Scenario Outline: Add zeros
+ Given the first number is
+ And the second number is
+ When the two numbers are added
+ Then the result should be
+Examples:
+ | first | second | result |
+ | 0 | 0 | 0 |
+ | 0 | 42 | 42 |
+@testOnly
+Examples:
+ | first | second | result |
+ | 42 | 0 | 42 |
+
+Scenario: Add negatives
+ Given the entered numbers are
+ | number |
+ | -5 |
+ | -7 |
+ When the two numbers are added
+ Then the result should be -12
+ And the text message should be
+ """
+ The result is -12.
+ """
+
diff --git a/Tests/VsExtConnectorTestSamples/VsExtConnectorTestSamples.ReqnrollCalculator.Tests/Features/Multiplication.feature b/Tests/VsExtConnectorTestSamples/VsExtConnectorTestSamples.ReqnrollCalculator.Tests/Features/Multiplication.feature
new file mode 100644
index 00000000..c3f7dd01
--- /dev/null
+++ b/Tests/VsExtConnectorTestSamples/VsExtConnectorTestSamples.ReqnrollCalculator.Tests/Features/Multiplication.feature
@@ -0,0 +1,20 @@
+Feature: Multiplication
+
+The calculator functions related to multiplication
+
+@core
+Rule: Numbers can be multiplied
+
+Scenario: Add two numbers
+ Given the first number is 5
+ And the second number is 7
+ When the two numbers are multiplied
+ Then the result should be 35
+
+Rule: Can multiply with zero
+
+Scenario: Multily with zero (wrong)
+ Given the first number is 5
+ And the second number is 0
+ When the two numbers are multiplied
+ Then the result should be 1
diff --git a/Tests/VsExtConnectorTestSamples/VsExtConnectorTestSamples.ReqnrollCalculator.Tests/ImplicitUsings.cs b/Tests/VsExtConnectorTestSamples/VsExtConnectorTestSamples.ReqnrollCalculator.Tests/ImplicitUsings.cs
new file mode 100644
index 00000000..d220047a
--- /dev/null
+++ b/Tests/VsExtConnectorTestSamples/VsExtConnectorTestSamples.ReqnrollCalculator.Tests/ImplicitUsings.cs
@@ -0,0 +1,2 @@
+global using AwesomeAssertions;
+global using Reqnroll;
diff --git a/Tests/VsExtConnectorTestSamples/VsExtConnectorTestSamples.ReqnrollCalculator.Tests/StepDefinitions/CalculatorStepDefinitions.cs b/Tests/VsExtConnectorTestSamples/VsExtConnectorTestSamples.ReqnrollCalculator.Tests/StepDefinitions/CalculatorStepDefinitions.cs
new file mode 100644
index 00000000..98bbeffd
--- /dev/null
+++ b/Tests/VsExtConnectorTestSamples/VsExtConnectorTestSamples.ReqnrollCalculator.Tests/StepDefinitions/CalculatorStepDefinitions.cs
@@ -0,0 +1,55 @@
+namespace ReqnrollCalculator.Specs.StepDefinitions;
+
+[Binding]
+public sealed class CalculatorStepDefinitions
+{
+ private readonly Calculator _calculator = new();
+
+ [Given("the first number is {int}")]
+ public void GivenTheFirstNumberIs(int number)
+ {
+ _calculator.Reset();
+ _calculator.Enter(number);
+ }
+
+ [Given("the second number is {int}")]
+ public void GivenTheSecondNumberIs(int number)
+ {
+ _calculator.Enter(number);
+ }
+
+ [Given("the entered numbers are")]
+ public void GivenTheEnteredNumbersAre(DataTable dataTable)
+ {
+ _calculator.Reset();
+ foreach (var row in dataTable.Rows)
+ {
+ _calculator.Enter(int.Parse(row[0]));
+ }
+ }
+
+ [When("the two numbers are added")]
+ public void WhenTheTwoNumbersAreAdded()
+ {
+ _calculator.Add();
+ }
+
+ [When("the two numbers are multiplied")]
+ public void WhenTheTwoNumbersAreMultiplied()
+ {
+ _calculator.Multiply();
+ }
+
+ [Then("the result should be {int}")]
+ public void ThenTheResultShouldBe(int result)
+ {
+ _calculator.GetResult().Should().Be(result);
+ }
+
+ [Then("the text message should be")]
+ public void ThenTheTextMessageShouldBe(string expectedMessage)
+ {
+ _calculator.GetResultMessage().Should().Be(expectedMessage);
+ }
+
+}
diff --git a/Tests/VsExtConnectorTestSamples/VsExtConnectorTestSamples.ReqnrollCalculator.Tests/VsExtConnectorTestSamples.ReqnrollCalculator.Tests.csproj b/Tests/VsExtConnectorTestSamples/VsExtConnectorTestSamples.ReqnrollCalculator.Tests/VsExtConnectorTestSamples.ReqnrollCalculator.Tests.csproj
new file mode 100644
index 00000000..7aded29d
--- /dev/null
+++ b/Tests/VsExtConnectorTestSamples/VsExtConnectorTestSamples.ReqnrollCalculator.Tests/VsExtConnectorTestSamples.ReqnrollCalculator.Tests.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+ MSTEST0001
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/VsExtConnectorTestSamples/VsExtConnectorTestSamples.ReqnrollCalculator.Tests/reqnroll.json b/Tests/VsExtConnectorTestSamples/VsExtConnectorTestSamples.ReqnrollCalculator.Tests/reqnroll.json
new file mode 100644
index 00000000..5168db9f
--- /dev/null
+++ b/Tests/VsExtConnectorTestSamples/VsExtConnectorTestSamples.ReqnrollCalculator.Tests/reqnroll.json
@@ -0,0 +1,3 @@
+{
+ "$schema": "https://schemas.reqnroll.net/reqnroll-config-latest.json"
+}
diff --git a/Tests/VsExtConnectorTestSamples/VsExtConnectorTestSamples.slnx b/Tests/VsExtConnectorTestSamples/VsExtConnectorTestSamples.slnx
new file mode 100644
index 00000000..3c1beb5f
--- /dev/null
+++ b/Tests/VsExtConnectorTestSamples/VsExtConnectorTestSamples.slnx
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/Tests/VsExtConnectorTestSamples/update-versions.ps1 b/Tests/VsExtConnectorTestSamples/update-versions.ps1
new file mode 100644
index 00000000..f9056bd4
--- /dev/null
+++ b/Tests/VsExtConnectorTestSamples/update-versions.ps1
@@ -0,0 +1,34 @@
+param(
+ [string] $version
+)
+
+function Set-Versions($content, $versionToSet) {
+
+
+ $newContent = [regex]::replace($content,'', {
+ param($m)
+ $m.Value.Replace($m.Groups["version"].Value, $versionToSet)
+ })
+ return $newContent
+}
+
+if (-not $version) {
+ Write-Error "Error. The version was not specified."
+ Write-Error "Usage:"
+ Write-Error " update-versions.ps1 [version]"
+ Exit
+}
+
+$projectFiles = Get-ChildItem -Path $PSScriptRoot -File -Recurse -Filter '*.*proj'
+$changedCount = 0
+foreach ($path in $projectFiles){
+ $fileContent = Get-Content -LiteralPath $path.FullName -Raw
+ $newFileContent = Set-Versions $fileContent $version
+ if ($newFileContent -ne $fileContent){
+ Write-Host "Updating $($path.Name)"
+ Set-Content -Path $path.FullName -Value $newFileContent -NoNewline -Encoding utf8
+ $changedCount++
+ }
+}
+
+Write-Host "Updated $changedCount files."