mirror of
https://github.com/actions/runner.git
synced 2026-03-12 17:57:13 -04:00
Add L0 tests for REPL parser and session routing
Parser tests (DapReplParserL0, 22 tests): - help: bare, case-insensitive, with topic - run: simple script, with shell, env, working_directory, all options - Edge cases: escaped quotes, commas in env values - Errors: empty args, unquoted arg, unknown option, missing paren - Non-DSL input falls through: expressions, wrapped expressions, empty - Help text contains expected commands and options - Internal helpers: SplitArguments with nested braces, empty env block Session integration tests (DapDebugSessionL0, 4 tests): - REPL help returns help text - REPL non-DSL input falls through to expression evaluation - REPL parse error returns error result (not a DAP error response) - watch context still evaluates expressions (not routed through REPL) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -968,5 +968,171 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region REPL routing tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task ReplHelpReturnsHelpText()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
await InitializeSessionAsync();
|
||||
|
||||
var evaluateJson = JsonConvert.SerializeObject(new Request
|
||||
{
|
||||
Seq = 10,
|
||||
Type = "request",
|
||||
Command = "evaluate",
|
||||
Arguments = Newtonsoft.Json.Linq.JObject.FromObject(new EvaluateArguments
|
||||
{
|
||||
Expression = "help",
|
||||
Context = "repl"
|
||||
})
|
||||
});
|
||||
_sentResponses.Clear();
|
||||
await _session.HandleMessageAsync(evaluateJson, CancellationToken.None);
|
||||
|
||||
Assert.Single(_sentResponses);
|
||||
Assert.True(_sentResponses[0].Success);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task ReplExpressionFallsThroughToEvaluation()
|
||||
{
|
||||
using (var hc = CreateTestContext())
|
||||
{
|
||||
await InitializeSessionAsync();
|
||||
_session.HandleClientConnected();
|
||||
|
||||
var exprValues = new DictionaryContextData();
|
||||
exprValues["github"] = new DictionaryContextData
|
||||
{
|
||||
{ "repository", new StringContextData("owner/repo") }
|
||||
};
|
||||
|
||||
var step = CreateMockStepWithEvaluatableContext(hc, "Run tests", exprValues);
|
||||
var jobContext = CreateMockJobContext();
|
||||
|
||||
var stepTask = _session.OnStepStartingAsync(step.Object, jobContext.Object, isFirstStep: true, CancellationToken.None);
|
||||
await Task.Delay(100);
|
||||
|
||||
// In REPL context, a non-DSL expression should still evaluate
|
||||
var evaluateJson = JsonConvert.SerializeObject(new Request
|
||||
{
|
||||
Seq = 20,
|
||||
Type = "request",
|
||||
Command = "evaluate",
|
||||
Arguments = Newtonsoft.Json.Linq.JObject.FromObject(new EvaluateArguments
|
||||
{
|
||||
Expression = "github.repository",
|
||||
Context = "repl"
|
||||
})
|
||||
});
|
||||
_sentResponses.Clear();
|
||||
await _session.HandleMessageAsync(evaluateJson, CancellationToken.None);
|
||||
|
||||
Assert.Single(_sentResponses);
|
||||
Assert.True(_sentResponses[0].Success);
|
||||
|
||||
var continueJson = JsonConvert.SerializeObject(new Request
|
||||
{
|
||||
Seq = 21,
|
||||
Type = "request",
|
||||
Command = "continue"
|
||||
});
|
||||
await _session.HandleMessageAsync(continueJson, CancellationToken.None);
|
||||
await Task.WhenAny(stepTask, Task.Delay(5000));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task ReplParseErrorReturnsErrorResult()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
await InitializeSessionAsync();
|
||||
|
||||
// Malformed run() command
|
||||
var evaluateJson = JsonConvert.SerializeObject(new Request
|
||||
{
|
||||
Seq = 10,
|
||||
Type = "request",
|
||||
Command = "evaluate",
|
||||
Arguments = Newtonsoft.Json.Linq.JObject.FromObject(new EvaluateArguments
|
||||
{
|
||||
Expression = "run()",
|
||||
Context = "repl"
|
||||
})
|
||||
});
|
||||
_sentResponses.Clear();
|
||||
await _session.HandleMessageAsync(evaluateJson, CancellationToken.None);
|
||||
|
||||
Assert.Single(_sentResponses);
|
||||
Assert.True(_sentResponses[0].Success);
|
||||
// The response is successful at the DAP level (not an error
|
||||
// response), but the result body conveys the parse error
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task WatchContextStillEvaluatesExpressions()
|
||||
{
|
||||
using (var hc = CreateTestContext())
|
||||
{
|
||||
await InitializeSessionAsync();
|
||||
_session.HandleClientConnected();
|
||||
|
||||
var exprValues = new DictionaryContextData();
|
||||
exprValues["github"] = new DictionaryContextData
|
||||
{
|
||||
{ "repository", new StringContextData("owner/repo") }
|
||||
};
|
||||
|
||||
var step = CreateMockStepWithEvaluatableContext(hc, "Run tests", exprValues);
|
||||
var jobContext = CreateMockJobContext();
|
||||
|
||||
var stepTask = _session.OnStepStartingAsync(step.Object, jobContext.Object, isFirstStep: true, CancellationToken.None);
|
||||
await Task.Delay(100);
|
||||
|
||||
// watch context should NOT route through REPL even if input
|
||||
// looks like a DSL command — it should evaluate as expression
|
||||
var evaluateJson = JsonConvert.SerializeObject(new Request
|
||||
{
|
||||
Seq = 20,
|
||||
Type = "request",
|
||||
Command = "evaluate",
|
||||
Arguments = Newtonsoft.Json.Linq.JObject.FromObject(new EvaluateArguments
|
||||
{
|
||||
Expression = "github.repository",
|
||||
Context = "watch"
|
||||
})
|
||||
});
|
||||
_sentResponses.Clear();
|
||||
await _session.HandleMessageAsync(evaluateJson, CancellationToken.None);
|
||||
|
||||
Assert.Single(_sentResponses);
|
||||
Assert.True(_sentResponses[0].Success);
|
||||
|
||||
var continueJson = JsonConvert.SerializeObject(new Request
|
||||
{
|
||||
Seq = 21,
|
||||
Type = "request",
|
||||
Command = "continue"
|
||||
});
|
||||
await _session.HandleMessageAsync(continueJson, CancellationToken.None);
|
||||
await Task.WhenAny(stepTask, Task.Delay(5000));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
314
src/Test/L0/Worker/DapReplParserL0.cs
Normal file
314
src/Test/L0/Worker/DapReplParserL0.cs
Normal file
@@ -0,0 +1,314 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using GitHub.Runner.Common.Tests;
|
||||
using GitHub.Runner.Worker.Dap;
|
||||
using Xunit;
|
||||
|
||||
namespace GitHub.Runner.Common.Tests.Worker
|
||||
{
|
||||
public sealed class DapReplParserL0
|
||||
{
|
||||
#region help command
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Parse_HelpReturnsHelpCommand()
|
||||
{
|
||||
var cmd = DapReplParser.TryParse("help", out var error);
|
||||
|
||||
Assert.Null(error);
|
||||
var help = Assert.IsType<HelpCommand>(cmd);
|
||||
Assert.Null(help.Topic);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Parse_HelpCaseInsensitive()
|
||||
{
|
||||
var cmd = DapReplParser.TryParse("Help", out var error);
|
||||
Assert.Null(error);
|
||||
Assert.IsType<HelpCommand>(cmd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Parse_HelpWithTopic()
|
||||
{
|
||||
var cmd = DapReplParser.TryParse("help(\"run\")", out var error);
|
||||
|
||||
Assert.Null(error);
|
||||
var help = Assert.IsType<HelpCommand>(cmd);
|
||||
Assert.Equal("run", help.Topic);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region run command — basic
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Parse_RunSimpleScript()
|
||||
{
|
||||
var cmd = DapReplParser.TryParse("run(\"echo hello\")", out var error);
|
||||
|
||||
Assert.Null(error);
|
||||
var run = Assert.IsType<RunCommand>(cmd);
|
||||
Assert.Equal("echo hello", run.Script);
|
||||
Assert.Null(run.Shell);
|
||||
Assert.Null(run.Env);
|
||||
Assert.Null(run.WorkingDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Parse_RunWithShell()
|
||||
{
|
||||
var cmd = DapReplParser.TryParse("run(\"echo hello\", shell: \"bash\")", out var error);
|
||||
|
||||
Assert.Null(error);
|
||||
var run = Assert.IsType<RunCommand>(cmd);
|
||||
Assert.Equal("echo hello", run.Script);
|
||||
Assert.Equal("bash", run.Shell);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Parse_RunWithWorkingDirectory()
|
||||
{
|
||||
var cmd = DapReplParser.TryParse("run(\"ls\", working_directory: \"/tmp\")", out var error);
|
||||
|
||||
Assert.Null(error);
|
||||
var run = Assert.IsType<RunCommand>(cmd);
|
||||
Assert.Equal("ls", run.Script);
|
||||
Assert.Equal("/tmp", run.WorkingDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Parse_RunWithEnv()
|
||||
{
|
||||
var cmd = DapReplParser.TryParse("run(\"echo $FOO\", env: { FOO: \"bar\" })", out var error);
|
||||
|
||||
Assert.Null(error);
|
||||
var run = Assert.IsType<RunCommand>(cmd);
|
||||
Assert.Equal("echo $FOO", run.Script);
|
||||
Assert.NotNull(run.Env);
|
||||
Assert.Equal("bar", run.Env["FOO"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Parse_RunWithMultipleEnvVars()
|
||||
{
|
||||
var cmd = DapReplParser.TryParse("run(\"echo\", env: { A: \"1\", B: \"2\" })", out var error);
|
||||
|
||||
Assert.Null(error);
|
||||
var run = Assert.IsType<RunCommand>(cmd);
|
||||
Assert.Equal(2, run.Env.Count);
|
||||
Assert.Equal("1", run.Env["A"]);
|
||||
Assert.Equal("2", run.Env["B"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Parse_RunWithAllOptions()
|
||||
{
|
||||
var input = "run(\"echo $X\", shell: \"zsh\", env: { X: \"1\" }, working_directory: \"/tmp\")";
|
||||
var cmd = DapReplParser.TryParse(input, out var error);
|
||||
|
||||
Assert.Null(error);
|
||||
var run = Assert.IsType<RunCommand>(cmd);
|
||||
Assert.Equal("echo $X", run.Script);
|
||||
Assert.Equal("zsh", run.Shell);
|
||||
Assert.Equal("1", run.Env["X"]);
|
||||
Assert.Equal("/tmp", run.WorkingDirectory);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region run command — edge cases
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Parse_RunWithEscapedQuotes()
|
||||
{
|
||||
var cmd = DapReplParser.TryParse("run(\"echo \\\"hello\\\"\")", out var error);
|
||||
|
||||
Assert.Null(error);
|
||||
var run = Assert.IsType<RunCommand>(cmd);
|
||||
Assert.Equal("echo \"hello\"", run.Script);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Parse_RunWithCommaInEnvValue()
|
||||
{
|
||||
var cmd = DapReplParser.TryParse("run(\"echo\", env: { CSV: \"a,b,c\" })", out var error);
|
||||
|
||||
Assert.Null(error);
|
||||
var run = Assert.IsType<RunCommand>(cmd);
|
||||
Assert.Equal("a,b,c", run.Env["CSV"]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region error cases
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Parse_RunEmptyArgsReturnsError()
|
||||
{
|
||||
var cmd = DapReplParser.TryParse("run()", out var error);
|
||||
|
||||
Assert.NotNull(error);
|
||||
Assert.Null(cmd);
|
||||
Assert.Contains("requires a script argument", error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Parse_RunUnquotedArgReturnsError()
|
||||
{
|
||||
var cmd = DapReplParser.TryParse("run(echo hello)", out var error);
|
||||
|
||||
Assert.NotNull(error);
|
||||
Assert.Null(cmd);
|
||||
Assert.Contains("quoted string", error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Parse_RunUnknownOptionReturnsError()
|
||||
{
|
||||
var cmd = DapReplParser.TryParse("run(\"echo\", timeout: \"10\")", out var error);
|
||||
|
||||
Assert.NotNull(error);
|
||||
Assert.Null(cmd);
|
||||
Assert.Contains("Unknown option", error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Parse_RunMissingClosingParenReturnsError()
|
||||
{
|
||||
var cmd = DapReplParser.TryParse("run(\"echo\"", out var error);
|
||||
|
||||
Assert.NotNull(error);
|
||||
Assert.Null(cmd);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region non-DSL input falls through
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Parse_ExpressionReturnsNull()
|
||||
{
|
||||
var cmd = DapReplParser.TryParse("github.repository", out var error);
|
||||
|
||||
Assert.Null(error);
|
||||
Assert.Null(cmd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Parse_WrappedExpressionReturnsNull()
|
||||
{
|
||||
var cmd = DapReplParser.TryParse("${{ github.event_name }}", out var error);
|
||||
|
||||
Assert.Null(error);
|
||||
Assert.Null(cmd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Parse_EmptyInputReturnsNull()
|
||||
{
|
||||
var cmd = DapReplParser.TryParse("", out var error);
|
||||
Assert.Null(error);
|
||||
Assert.Null(cmd);
|
||||
|
||||
cmd = DapReplParser.TryParse(null, out error);
|
||||
Assert.Null(error);
|
||||
Assert.Null(cmd);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region help text
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void GetGeneralHelp_ContainsCommands()
|
||||
{
|
||||
var help = DapReplParser.GetGeneralHelp();
|
||||
|
||||
Assert.Contains("help", help);
|
||||
Assert.Contains("run", help);
|
||||
Assert.Contains("expression", help, System.StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void GetRunHelp_ContainsOptions()
|
||||
{
|
||||
var help = DapReplParser.GetRunHelp();
|
||||
|
||||
Assert.Contains("shell", help);
|
||||
Assert.Contains("env", help);
|
||||
Assert.Contains("working_directory", help);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region internal parser helpers
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void SplitArguments_HandlesNestedBraces()
|
||||
{
|
||||
var args = DapReplParser.SplitArguments("\"hello\", env: { A: \"1\", B: \"2\" }", out var error);
|
||||
|
||||
Assert.Null(error);
|
||||
Assert.Equal(2, args.Count);
|
||||
Assert.Equal("\"hello\"", args[0].Trim());
|
||||
Assert.Contains("A:", args[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void ParseEnvBlock_HandlesEmptyBlock()
|
||||
{
|
||||
var result = DapReplParser.ParseEnvBlock("{ }", out var error);
|
||||
|
||||
Assert.Null(error);
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user